tksbrokerapi.TKSBrokerAPI
TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios,
as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
from the console, it has a rich keys and commands, or you can use it as Python module with python import.
TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
- Open account for trading: http://tinkoff.ru/sl/AaX1Et1omnH
- TKSBrokerAPI module documentation: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
- See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
- Used constants are in the TKSEnums module: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
- About Tinkoff Invest API: https://tinkoff.github.io/investAPI/
- Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/
1# -*- coding: utf-8 -*- 2# Author: Timur Gilmullin 3 4""" 5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios, 6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: 7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`. 8 9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive 10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems. 11 12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH 13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html 14- **See examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples 15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html 16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/ 17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/ 18""" 19 20# Copyright (c) 2022 Gilmillin Timur Mansurovich 21# 22# Licensed under the Apache License, Version 2.0 (the "License"); 23# you may not use this file except in compliance with the License. 24# You may obtain a copy of the License at 25# 26# http://www.apache.org/licenses/LICENSE-2.0 27# 28# Unless required by applicable law or agreed to in writing, software 29# distributed under the License is distributed on an "AS IS" BASIS, 30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 31# See the License for the specific language governing permissions and 32# limitations under the License. 33 34 35import sys 36import os 37from argparse import ArgumentParser 38from importlib.metadata import version 39 40from datetime import datetime, timedelta 41from dateutil.tz import tzlocal, tzutc 42from time import sleep 43 44import re 45import json 46import requests 47import traceback as tb 48from typing import Union 49 50from multiprocessing import cpu_count 51from multiprocessing.pool import ThreadPool 52import pandas as pd 53 54from TKSEnums import * # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/ 55from TradeRoutines import * # This library contains some methods used by trade scenarios implemented with TKSBrokerAPI module 56 57from pricegenerator.PriceGenerator import PriceGenerator, uLogger # This module has a lot of instruments to work with candles data. See docs here: https://github.com/Tim55667757/PriceGenerator 58from pricegenerator.UniLogger import DisableLogger as PGDisLog # Method for disable log from PriceGenerator 59 60import UniLogger as uLog # Logger for TKSBrokerAPI 61 62 63# --- Common technical parameters: 64 65PGDisLog(uLogger.handlers[0]) # Disable 3-rd party logging from PriceGenerator 66uLogger = uLog.UniLogger # init logger for TKSBrokerAPI 67uLogger.level = 10 # debug level by default for TKSBrokerAPI module 68uLogger.handlers[0].level = 20 # info level by default for STDOUT of TKSBrokerAPI module 69 70__version__ = "1.5" # The "major.minor" version setup here, but build number define at the build-server only 71 72CPU_COUNT = cpu_count() # host's real CPU count 73CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1 # how many CPUs will be used for parallel calculations 74 75 76def GetDatesAsString(start: str = None, end: str = None) -> tuple: 77 """ 78 Create tuple of date and time strings with timezone parsed from user-friendly date. 79 80 User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020). 81 82 Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") 83 An error exception will occur if input date has incorrect format. 84 85 If `start=None`, `end=None` then return dates from yesterday to the end of the day. 86 If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day. 87 If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`. 88 Start day may be negative integer numbers: `-1`, `-2`, `-3` — how many days ago. 89 90 Also, you can use keywords for start if `end=None`: 91 `today` (from 00:00:00 to the end of current day), 92 `yesterday` (-1 day from 00:00:00 to 23:59:59), 93 `week` (-7 day from 00:00:00 to the end of current day), 94 `month` (-30 day from 00:00:00 to the end of current day), 95 `year` (-365 day from 00:00:00 to the end of current day), 96 97 :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI. 98 See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`. 99 Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day. 100 """ 101 uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end)) 102 s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0) # start of the current day 103 e = s.replace(hour=23, minute=59, second=59, microsecond=0) # end of the current day 104 105 # time between start and the end of the current day: 106 if start is None or start.lower() == "today": 107 pass 108 109 # from start of the last day to the end of the last day: 110 elif start.lower() == "yesterday": 111 s -= timedelta(days=1) 112 e -= timedelta(days=1) 113 114 # week (-7 day from 00:00:00 to the end of the current day): 115 elif start.lower() == "week": 116 s -= timedelta(days=6) # +1 current day already taken into account 117 118 # month (-30 day from 00:00:00 to the end of current day): 119 elif start.lower() == "month": 120 s -= timedelta(days=29) # +1 current day already taken into account 121 122 # year (-365 day from 00:00:00 to the end of current day): 123 elif start.lower() == "year": 124 s -= timedelta(days=364) # +1 current day already taken into account 125 126 # -N days ago to the end of current day: 127 elif start.startswith('-') and start[1:].isdigit(): 128 s -= timedelta(days=abs(int(start)) - 1) # +1 current day already taken into account 129 130 # dates between start day at 00:00:00 and the end of the last day at 23:59:59: 131 else: 132 s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc()) 133 e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e 134 135 # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API: 136 s = s.strftime(TKS_DATE_TIME_FORMAT) 137 e = e.strftime(TKS_DATE_TIME_FORMAT) 138 139 uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e)) 140 141 return s, e 142 143 144class TinkoffBrokerServer: 145 """ 146 This class implements methods to work with Tinkoff broker server. 147 148 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 149 150 About `token`: https://tinkoff.github.io/investAPI/token/ 151 """ 152 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 153 """ 154 Main class init. 155 156 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 157 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 158 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 159 :param useCache: use default cache file with raw data to use instead of `iList`. 160 True by default. Cache is auto-update if new day has come. 161 If you don't want to use cache and always updates raw data then set `useCache=False`. 162 :param defaultCache: path to default cache file. `dump.json` by default. 163 """ 164 if token is None or not token: 165 try: 166 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 167 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 168 169 except KeyError: 170 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 171 raise Exception("Token required") 172 173 else: 174 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 175 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 176 177 if accountId is None or not accountId: 178 try: 179 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 180 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 181 182 except KeyError: 183 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 184 185 else: 186 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 187 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 188 189 self.version = __version__ # duplicate here used TKSBrokerAPI main version 190 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 191 192 Latest version: https://pypi.org/project/tksbrokerapi/ 193 """ 194 195 self.aliases = TKS_TICKER_ALIASES 196 """Some aliases instead official tickers. 197 198 See also: `TKSEnums.TKS_TICKER_ALIASES` 199 """ 200 201 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 202 203 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 204 205 self.ticker = "" 206 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 207 208 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 209 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 210 211 See also: `SearchByTicker()`, `SearchInstruments()`. 212 """ 213 214 self.figi = "" 215 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 216 217 See also: `SearchByFIGI()`, `SearchInstruments()`. 218 """ 219 220 self.depth = 1 221 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 222 223 See also: `GetCurrentPrices()`. 224 """ 225 226 self.server = r"https://invest-public-api.tinkoff.ru/rest" 227 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 228 229 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 230 """ 231 232 uLogger.debug("Broker API server: {}".format(self.server)) 233 234 self.timeout = 15 235 """Server operations timeout in seconds. Default: `15`. 236 237 See also: `SendAPIRequest()`. 238 """ 239 240 self.headers = { 241 "Content-Type": "application/json", 242 "accept": "application/json", 243 "Authorization": "Bearer {}".format(self.token), 244 "x-app-name": "Tim55667757.TKSBrokerAPI", 245 } 246 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 247 248 See also: `SendAPIRequest()`. 249 """ 250 251 self.body = None 252 """Request body which send to broker server. Default: `None`. 253 254 See also: `SendAPIRequest()`. 255 """ 256 257 self.moreDebug = False 258 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 259 260 self.historyFile = None 261 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 262 263 See also: `History()`. 264 """ 265 266 self.htmlHistoryFile = "index.html" 267 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 268 269 See also: `ShowHistoryChart()`. 270 """ 271 272 self.instrumentsFile = "instruments.md" 273 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 274 275 See also: `ShowInstrumentsInfo()`. 276 """ 277 278 self.searchResultsFile = "search-results.md" 279 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 280 281 See also: `SearchInstruments()`. 282 """ 283 284 self.pricesFile = "prices.md" 285 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 286 287 See also: `GetListOfPrices()`. 288 """ 289 290 self.infoFile = "info.md" 291 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 292 293 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 294 """ 295 296 self.bondsXLSXFile = "ext-bonds.xlsx" 297 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 298 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 299 300 See also: `ExtendBondsData()`. 301 """ 302 303 self.calendarFile = "calendar.md" 304 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 305 306 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 307 308 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 309 """ 310 311 self.overviewFile = "overview.md" 312 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 313 314 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 315 """ 316 317 self.overviewDigestFile = "overview-digest.md" 318 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 319 320 See also: `Overview()` with parameter `details="digest"`. 321 """ 322 323 self.overviewPositionsFile = "overview-positions.md" 324 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 325 326 See also: `Overview()` with parameter `details="positions"`. 327 """ 328 329 self.overviewOrdersFile = "overview-orders.md" 330 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 331 332 See also: `Overview()` with parameter `details="orders"`. 333 """ 334 335 self.overviewAnalyticsFile = "overview-analytics.md" 336 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 337 338 See also: `Overview()` with parameter `details="analytics"`. 339 """ 340 341 self.overviewBondsCalendarFile = "overview-calendar.md" 342 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 343 344 See also: `Overview()` with parameter `details="calendar"`. 345 """ 346 347 self.reportFile = "deals.md" 348 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 349 350 See also: `Deals()`. 351 """ 352 353 self.withdrawalLimitsFile = "limits.md" 354 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 355 356 See also: `OverviewLimits()` and `RequestLimits()`. 357 """ 358 359 self.userInfoFile = "user-info.md" 360 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 361 362 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 363 """ 364 365 self.userAccountsFile = "accounts.md" 366 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 367 368 See also: `OverviewAccounts()`, `RequestAccounts()`. 369 """ 370 371 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 372 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 373 374 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 375 376 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 377 """ 378 379 self.iList = None # init iList for raw instruments data 380 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 381 382 See also: `Listing()`, `DumpInstruments()`. 383 """ 384 385 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 386 if useCache: 387 if os.path.exists(self.iListDumpFile): 388 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 389 curTime = datetime.now(tzutc()) 390 391 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 392 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 393 394 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 395 396 else: 397 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 398 399 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 400 os.path.abspath(self.iListDumpFile), 401 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 402 )) 403 404 else: 405 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 406 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 407 408 else: 409 self.iList = self.Listing() # request new raw instruments data from broker server 410 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 411 412 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 413 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 414 415 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 416 """ 417 418 def _ParseJSON(self, rawData="{}") -> dict: 419 """ 420 Parse JSON from response string. 421 422 :param rawData: this is a string with JSON-formatted text. 423 :return: JSON (dictionary), parsed from server response string. 424 """ 425 responseJSON = json.loads(rawData) if rawData else {} 426 427 if self.moreDebug: 428 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 429 430 return responseJSON 431 432 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 433 """ 434 Send GET or POST request to broker server and receive JSON object. 435 436 self.header: must be defining with dictionary of headers. 437 self.body: if define then used as request body. None by default. 438 self.timeout: global request timeout, 15 seconds by default. 439 :param url: url with REST request. 440 :param reqType: send "GET" or "POST" request. "GET" by default. 441 :param retry: how many times retry after first request if an 5xx server errors occurred. 442 :param pause: sleep time in seconds between retries. 443 :return: response JSON (dictionary) from broker. 444 """ 445 if reqType not in ("GET", "POST"): 446 uLogger.error("You can define request type: 'GET' or 'POST'!") 447 raise Exception("Incorrect value") 448 449 if self.moreDebug: 450 uLogger.debug("Request parameters:") 451 uLogger.debug(" - REST API URL: {}".format(url)) 452 uLogger.debug(" - request type: {}".format(reqType)) 453 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 454 uLogger.debug(" - body:\n{}".format(self.body)) 455 456 # fast hack to avoid all operations with some tickers/FIGI 457 responseJSON = {} 458 oK = True 459 for item in self.exclude: 460 if item in url: 461 if self.moreDebug: 462 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 463 464 oK = False 465 break 466 467 if oK: 468 counter = 0 469 response = None 470 errMsg = "" 471 472 while not response and counter <= retry: 473 if reqType == "GET": 474 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 475 476 if reqType == "POST": 477 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 478 479 if self.moreDebug: 480 uLogger.debug("Response:") 481 uLogger.debug(" - status code: {}".format(response.status_code)) 482 uLogger.debug(" - reason: {}".format(response.reason)) 483 uLogger.debug(" - body length: {}".format(len(response.text))) 484 uLogger.debug(" - headers:\n{}".format(response.headers)) 485 486 # Server returns some headers: 487 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 488 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 489 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 490 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 491 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 492 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 493 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 494 sleep(rateLimitWait) 495 496 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 497 if 400 <= response.status_code < 500: 498 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 499 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 500 counter = retry + 1 501 502 if 500 <= response.status_code < 600: 503 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 504 uLogger.debug(" - not oK, {}".format(errMsg)) 505 counter += 1 506 507 if counter <= retry: 508 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 509 sleep(pause) 510 511 responseJSON = self._ParseJSON(rawData=response.text) 512 513 if errMsg: 514 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 515 uLogger.error(" - not oK, {}".format(errMsg)) 516 517 return responseJSON 518 519 def _IUpdater(self, iType: str) -> tuple: 520 """ 521 Request instrument by type from server. See available API methods for instruments: 522 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 523 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 524 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 525 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 526 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 527 528 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 529 :return: tuple with iType name and list of available instruments of current type for defined user token. 530 """ 531 result = [] 532 533 if iType in TKS_INSTRUMENTS: 534 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 535 536 # all instruments have the same body in API v2 requests: 537 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 538 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 539 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 540 541 return iType, result 542 543 def _IWrapper(self, kwargs): 544 """ 545 Wrapper runs instrument's update method `_IUpdater()`. 546 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 547 """ 548 return self._IUpdater(**kwargs) 549 550 def Listing(self) -> dict: 551 """ 552 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 553 554 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 555 """ 556 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 557 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 558 559 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 560 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 561 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 562 563 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 564 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 565 poolUpdater.close() 566 567 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 568 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 569 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 570 571 # calculate minimum price increment (step) for all instruments and set up instrument's type: 572 for iType in iList.keys(): 573 for ticker in iList[iType]: 574 iList[iType][ticker]["type"] = iType 575 576 if "minPriceIncrement" in iList[iType][ticker].keys(): 577 iList[iType][ticker]["step"] = NanoToFloat( 578 iList[iType][ticker]["minPriceIncrement"]["units"], 579 iList[iType][ticker]["minPriceIncrement"]["nano"], 580 ) 581 582 else: 583 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 584 585 return iList 586 587 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 588 """ 589 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 590 591 See also: `DumpInstruments()`, `Listing()`. 592 593 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 594 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 595 """ 596 if self.iListDumpFile is None or not self.iListDumpFile: 597 uLogger.error("Output name of dump file must be defined!") 598 raise Exception("Filename required") 599 600 if not self.iList or forceUpdate: 601 self.iList = self.Listing() 602 603 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 604 605 # Save as XLSX with separated sheets for every type of instruments: 606 with pd.ExcelWriter( 607 path=xlsxDumpFile, 608 date_format=TKS_DATE_FORMAT, 609 datetime_format=TKS_DATE_TIME_FORMAT, 610 mode="w", 611 ) as writer: 612 for iType in TKS_INSTRUMENTS: 613 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 614 df = df[sorted(df)] # sorted by column names 615 df = df.applymap( 616 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 617 na_action="ignore", 618 ) # converting numbers from nano-type to float in every cell 619 df.to_excel( 620 writer, 621 sheet_name=iType, 622 encoding="UTF-8", 623 freeze_panes=(1, 1), 624 ) # saving as XLSX-file with freeze first row and column as headers 625 626 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 627 628 def DumpInstruments(self, forceUpdate: bool = True) -> str: 629 """ 630 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 631 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 632 633 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 634 635 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 636 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 637 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 638 """ 639 if self.iListDumpFile is None or not self.iListDumpFile: 640 uLogger.error("Output name of dump file must be defined!") 641 raise Exception("Filename required") 642 643 if not self.iList or forceUpdate: 644 self.iList = self.Listing() 645 646 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 647 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 648 fH.write(jsonDump) 649 650 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 651 652 return jsonDump 653 654 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 655 """ 656 Show information about one instrument defined by json data and prints it in Markdown format. 657 658 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 659 660 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 661 :param show: if `True` then also printing information about instrument and its current price. 662 :return: multilines text in Markdown format with information about one instrument. 663 """ 664 splitLine = "| | |\n" 665 infoText = "" 666 667 if iJSON is not None and iJSON and isinstance(iJSON, dict): 668 info = [ 669 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 670 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 671 "| Parameters | Values |\n", 672 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 673 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 674 "| Full name: | {:<54} |\n".format(iJSON["name"]), 675 ] 676 677 if "sector" in iJSON.keys() and iJSON["sector"]: 678 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 679 680 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 681 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 682 683 info.extend([ 684 splitLine, 685 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 686 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 687 ]) 688 689 if "isin" in iJSON.keys() and iJSON["isin"]: 690 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 691 692 if "classCode" in iJSON.keys(): 693 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 694 695 info.extend([ 696 splitLine, 697 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 698 splitLine, 699 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 700 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 701 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 702 ]) 703 704 if iJSON["figi"]: 705 self.figi = iJSON["figi"] 706 iJSON = iJSON | self.RequestTradingStatus() 707 708 info.extend([ 709 splitLine, 710 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 711 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 712 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 713 ]) 714 715 info.append(splitLine) 716 717 if "type" in iJSON.keys() and iJSON["type"]: 718 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 719 720 if "shareType" in iJSON.keys() and iJSON["shareType"]: 721 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 722 723 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 724 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 725 726 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 727 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 728 729 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 730 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 731 732 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 733 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 734 735 if "focusType" in iJSON.keys() and iJSON["focusType"]: 736 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 737 738 if "assetType" in iJSON.keys() and iJSON["assetType"]: 739 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 740 741 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 742 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 743 744 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 745 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 746 747 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 748 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 749 750 if "currency" in iJSON.keys(): 751 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 752 753 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 754 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 755 756 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 757 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 758 759 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 760 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 761 762 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 763 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 764 765 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 766 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 767 768 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 769 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 770 771 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 772 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 773 774 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 775 info.append("| Perpetual bond: | Yes |\n") 776 777 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 778 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 779 780 iExt = None 781 if iJSON["type"] == "Bonds": 782 info.extend([ 783 splitLine, 784 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 785 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 786 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 787 iJSON["nominal"]["currency"], 788 )), 789 ]) 790 791 if "floatingCouponFlag" in iJSON.keys(): 792 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 793 794 if "amortizationFlag" in iJSON.keys(): 795 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 796 797 info.append(splitLine) 798 799 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 800 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 801 802 if iJSON["figi"]: 803 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 804 805 info.extend([ 806 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 807 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 808 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 809 ]) 810 811 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 812 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 813 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 814 iJSON["aciValue"]["currency"] 815 ))) 816 817 if "currentPrice" in iJSON.keys(): 818 info.append(splitLine) 819 820 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 821 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 822 823 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 824 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 825 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 826 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 827 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 828 829 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 830 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 831 832 info.extend([ 833 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 834 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 835 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 836 )), 837 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 838 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 839 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 840 )), 841 "| Changes between last deal price and last close | {:<54} |\n".format( 842 "{:.2f}%{}".format( 843 iJSON["currentPrice"]["changes"], 844 " ({}{:.2f} {})".format( 845 "+" if bondChangesDelta > 0 else "", 846 bondChangesDelta, 847 aciCurrency 848 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 849 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 850 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 851 currency 852 ), 853 ) 854 ), 855 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 856 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 857 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 858 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 859 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 860 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 861 )), 862 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 863 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 864 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 865 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 866 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 867 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 868 )), 869 ]) 870 871 if "lot" in iJSON.keys(): 872 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 873 874 if "step" in iJSON.keys() and iJSON["step"] != 0: 875 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 876 877 # Add bond payment calendar: 878 if iJSON["type"] == "Bonds": 879 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 880 info.extend(["\n", strCalendar]) 881 882 infoText += "".join(info) 883 884 if show: 885 uLogger.info("{}".format(infoText)) 886 887 else: 888 uLogger.debug("{}".format(infoText)) 889 890 if self.infoFile is not None: 891 with open(self.infoFile, "w", encoding="UTF-8") as fH: 892 fH.write(infoText) 893 894 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 895 896 return infoText 897 898 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 899 """ 900 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 901 902 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 903 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 904 :return: JSON formatted data with information about instrument. 905 """ 906 tickerJSON = {} 907 if self.moreDebug: 908 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 909 910 if not self.ticker: 911 uLogger.warning("self.ticker variable is not be empty!") 912 913 else: 914 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 915 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 916 raise Exception("Instrument not allowed") 917 918 if not self.iList: 919 self.iList = self.Listing() 920 921 if self.ticker in self.iList["Shares"].keys(): 922 tickerJSON = self.iList["Shares"][self.ticker] 923 if self.moreDebug: 924 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 925 926 elif self.ticker in self.iList["Currencies"].keys(): 927 tickerJSON = self.iList["Currencies"][self.ticker] 928 if self.moreDebug: 929 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 930 931 elif self.ticker in self.iList["Bonds"].keys(): 932 tickerJSON = self.iList["Bonds"][self.ticker] 933 if self.moreDebug: 934 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 935 936 elif self.ticker in self.iList["Etfs"].keys(): 937 tickerJSON = self.iList["Etfs"][self.ticker] 938 if self.moreDebug: 939 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 940 941 elif self.ticker in self.iList["Futures"].keys(): 942 tickerJSON = self.iList["Futures"][self.ticker] 943 if self.moreDebug: 944 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 945 946 if tickerJSON: 947 self.figi = tickerJSON["figi"] 948 949 if requestPrice: 950 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 951 952 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 953 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 954 955 else: 956 tickerJSON["currentPrice"]["changes"] = 0 957 958 if show: 959 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 960 961 else: 962 if show: 963 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 964 965 return tickerJSON 966 967 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 968 """ 969 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 970 971 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 972 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 973 :return: JSON formatted data with information about instrument. 974 """ 975 figiJSON = {} 976 if self.moreDebug: 977 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 978 979 if not self.figi: 980 uLogger.warning("self.figi variable is not be empty!") 981 982 else: 983 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 984 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 985 raise Exception("Instrument not allowed") 986 987 if not self.iList: 988 self.iList = self.Listing() 989 990 for item in self.iList["Shares"].keys(): 991 if self.figi == self.iList["Shares"][item]["figi"]: 992 figiJSON = self.iList["Shares"][item] 993 994 if self.moreDebug: 995 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 996 997 break 998 999 if not figiJSON: 1000 for item in self.iList["Currencies"].keys(): 1001 if self.figi == self.iList["Currencies"][item]["figi"]: 1002 figiJSON = self.iList["Currencies"][item] 1003 1004 if self.moreDebug: 1005 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1006 1007 break 1008 1009 if not figiJSON: 1010 for item in self.iList["Bonds"].keys(): 1011 if self.figi == self.iList["Bonds"][item]["figi"]: 1012 figiJSON = self.iList["Bonds"][item] 1013 1014 if self.moreDebug: 1015 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1016 1017 break 1018 1019 if not figiJSON: 1020 for item in self.iList["Etfs"].keys(): 1021 if self.figi == self.iList["Etfs"][item]["figi"]: 1022 figiJSON = self.iList["Etfs"][item] 1023 1024 if self.moreDebug: 1025 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1026 1027 break 1028 1029 if not figiJSON: 1030 for item in self.iList["Futures"].keys(): 1031 if self.figi == self.iList["Futures"][item]["figi"]: 1032 figiJSON = self.iList["Futures"][item] 1033 1034 if self.moreDebug: 1035 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1036 1037 break 1038 1039 if figiJSON: 1040 self.figi = figiJSON["figi"] 1041 self.ticker = figiJSON["ticker"] 1042 1043 if requestPrice: 1044 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1045 1046 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1047 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1048 1049 else: 1050 figiJSON["currentPrice"]["changes"] = 0 1051 1052 if show: 1053 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1054 1055 else: 1056 if show: 1057 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1058 1059 return figiJSON 1060 1061 def GetCurrentPrices(self, show: bool = True) -> dict: 1062 """ 1063 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1064 `{"buy": [{"price": 1243.8, "quantity": 193}, 1065 {"price": 1244.0, "quantity": 168}, 1066 {"price": 1244.8, "quantity": 5}, 1067 {"price": 1245.0, "quantity": 61}, 1068 {"price": 1245.4, "quantity": 60}], 1069 "sell": [{"price": 1243.6, "quantity": 8}, 1070 {"price": 1242.6, "quantity": 10}, 1071 {"price": 1242.4, "quantity": 18}, 1072 {"price": 1242.2, "quantity": 50}, 1073 {"price": 1242.0, "quantity": 113}], 1074 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1075 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1076 - sell: list of dicts with Buyers prices, 1077 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1078 - quantity: volume value by current price in lots, 1079 - limitUp: current trade session limit price, maximum, 1080 - limitDown: current trade session limit price, minimum, 1081 - lastPrice: last deal price of the instrument, 1082 - closePrice: previous trade session close price of the instrument. 1083 1084 See also: `SearchByTicker()` and `SearchByFIGI()`. 1085 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1086 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1087 1088 :param show: if `True` then print DOM to log and console. 1089 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1090 If an error occurred then returns an empty record: 1091 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1092 """ 1093 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1094 1095 if self.depth < 1: 1096 uLogger.error("Depth of Market (DOM) must be >=1!") 1097 raise Exception("Incorrect value") 1098 1099 if not (self.ticker or self.figi): 1100 uLogger.error("self.ticker or self.figi variables must be defined!") 1101 raise Exception("Ticker or FIGI required") 1102 1103 if self.ticker and not self.figi: 1104 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1105 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1106 1107 if not self.ticker and self.figi: 1108 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1109 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1110 1111 if not self.figi: 1112 uLogger.error("FIGI is not defined!") 1113 raise Exception("Ticker or FIGI required") 1114 1115 else: 1116 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1117 1118 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1119 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1120 self.body = str({"figi": self.figi, "depth": self.depth}) 1121 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1122 1123 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1124 # list of dicts with sellers orders: 1125 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1126 1127 # list of dicts with buyers orders: 1128 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1129 1130 # max price of instrument at this time: 1131 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1132 1133 # min price of instrument at this time: 1134 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1135 1136 # last price of deal with instrument: 1137 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1138 1139 # last close price of instrument: 1140 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1141 1142 else: 1143 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1144 uLogger.debug("Server response: {}".format(pricesResponse)) 1145 1146 if show: 1147 if prices["buy"] or prices["sell"]: 1148 info = [ 1149 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1150 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1151 self.ticker, 1152 self.figi, 1153 self.depth, 1154 ), 1155 "-" * 60, "\n", 1156 " Orders of Buyers | Orders of Sellers\n", 1157 "-" * 60, "\n", 1158 " Sell prices (volumes) | Buy prices (volumes)\n", 1159 "-" * 60, "\n", 1160 ] 1161 1162 if not prices["buy"]: 1163 info.append(" | No orders!\n") 1164 sumBuy = 0 1165 1166 else: 1167 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1168 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1169 for item in maxMinSorted: 1170 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1171 1172 if not prices["sell"]: 1173 info.append("No orders! |\n") 1174 sumSell = 0 1175 1176 else: 1177 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1178 for item in prices["sell"]: 1179 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1180 1181 info.extend([ 1182 "-" * 60, "\n", 1183 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1184 "-" * 60, "\n", 1185 ]) 1186 1187 infoText = "".join(info) 1188 1189 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1190 1191 else: 1192 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1193 1194 return prices 1195 1196 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1197 """ 1198 This method get and show information about all available broker instruments for current user account. 1199 If `instrumentsFile` string is not empty then also save information to this file. 1200 1201 :param show: if `True` then print results to console, if `False` — print only to file. 1202 :return: multi-lines string with all available broker instruments 1203 """ 1204 if not self.iList: 1205 self.iList = self.Listing() 1206 1207 info = [ 1208 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1209 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1210 ] 1211 1212 # add instruments count by type: 1213 for iType in self.iList.keys(): 1214 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1215 1216 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1217 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1218 1219 # generating info tables with all instruments by type: 1220 for iType in self.iList.keys(): 1221 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1222 1223 for instrument in self.iList[iType].keys(): 1224 iName = self.iList[iType][instrument]["name"] # instrument's name 1225 if len(iName) > 57: 1226 iName = "{}...".format(iName[:54]) # right trim for a long string 1227 1228 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1229 self.iList[iType][instrument]["ticker"], 1230 iName, 1231 self.iList[iType][instrument]["figi"], 1232 self.iList[iType][instrument]["currency"], 1233 self.iList[iType][instrument]["lot"], 1234 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1235 )) 1236 1237 infoText = "".join(info) 1238 1239 if show: 1240 uLogger.info(infoText) 1241 1242 if self.instrumentsFile: 1243 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1244 fH.write(infoText) 1245 1246 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1247 1248 return infoText 1249 1250 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1251 """ 1252 This method search and show information about instruments by part of its ticker, FIGI or name. 1253 If `searchResultsFile` string is not empty then also save information to this file. 1254 1255 :param pattern: string with part of ticker, FIGI or instrument's name. 1256 :param show: if `True` then print results to console, if `False` — return list of result only. 1257 :return: list of dictionaries with all found instruments. 1258 """ 1259 if not self.iList: 1260 self.iList = self.Listing() 1261 1262 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1263 compiledPattern = re.compile(pattern, re.IGNORECASE) 1264 1265 for iType in self.iList: 1266 for instrument in self.iList[iType].values(): 1267 searchResult = compiledPattern.search(" ".join( 1268 [instrument["ticker"], instrument["figi"], instrument["name"]] 1269 )) 1270 1271 if searchResult: 1272 searchResults[iType][instrument["ticker"]] = instrument 1273 1274 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1275 info = [ 1276 "# Search results\n\n", 1277 "* **Search pattern:** [{}]\n".format(pattern), 1278 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1279 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1280 ] 1281 infoShort = info[:] 1282 1283 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1284 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1285 skippedLine = "| ... | ... | ... | ... |\n" 1286 1287 if resultsLen == 0: 1288 info.append("\nNo results\n") 1289 infoShort.append("\nNo results\n") 1290 uLogger.warning("No results. Try changing your search pattern.") 1291 1292 else: 1293 for iType in searchResults: 1294 iTypeValuesCount = len(searchResults[iType].values()) 1295 if iTypeValuesCount > 0: 1296 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1297 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1298 1299 for instrument in searchResults[iType].values(): 1300 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1301 instrument["type"], 1302 instrument["ticker"], 1303 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1304 instrument["figi"], 1305 )) 1306 1307 if iTypeValuesCount <= 5: 1308 infoShort.extend(info[-iTypeValuesCount:]) 1309 1310 else: 1311 infoShort.extend(info[-5:]) 1312 infoShort.append(skippedLine) 1313 1314 infoText = "".join(info) 1315 infoTextShort = "".join(infoShort) 1316 1317 if show: 1318 uLogger.info(infoTextShort) 1319 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1320 1321 if self.searchResultsFile: 1322 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1323 fH.write(infoText) 1324 1325 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1326 1327 return searchResults 1328 1329 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1330 """ 1331 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1332 1333 :param instruments: list of strings with tickers or FIGIs. 1334 :return: list with unique instrument FIGIs only. 1335 """ 1336 requestedInstruments = [] 1337 for iName in instruments: 1338 if iName not in self.aliases.keys(): 1339 if iName not in requestedInstruments: 1340 requestedInstruments.append(iName) 1341 1342 else: 1343 if iName not in requestedInstruments: 1344 if self.aliases[iName] not in requestedInstruments: 1345 requestedInstruments.append(self.aliases[iName]) 1346 1347 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1348 1349 onlyUniqueFIGIs = [] 1350 for iName in requestedInstruments: 1351 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1352 continue 1353 1354 self.ticker = iName 1355 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1356 1357 if not iData: 1358 self.ticker = "" 1359 self.figi = iName 1360 1361 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1362 1363 if not iData: 1364 self.figi = "" 1365 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1366 1367 if iData and iData["figi"] not in onlyUniqueFIGIs: 1368 onlyUniqueFIGIs.append(iData["figi"]) 1369 1370 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1371 1372 return onlyUniqueFIGIs 1373 1374 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1375 """ 1376 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1377 1378 See limits: https://tinkoff.github.io/investAPI/limits/ 1379 1380 If `pricesFile` string is not empty then also save information to this file. 1381 1382 :param instruments: list of strings with tickers or FIGIs. 1383 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1384 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1385 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1386 """ 1387 if instruments is None or not instruments: 1388 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1389 raise Exception("Ticker or FIGI required") 1390 1391 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1392 1393 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1394 1395 iList = [] # trying to get info and current prices about all unique instruments: 1396 for self.figi in onlyUniqueFIGIs: 1397 iData = self.SearchByFIGI(requestPrice=True) 1398 iList.append(iData) 1399 1400 self.ShowListOfPrices(iList, show) 1401 1402 return iList 1403 1404 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1405 """ 1406 Show table contains current prices of given instruments. 1407 1408 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1409 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1410 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1411 :return: multilines text in Markdown format as a table contains current prices. 1412 """ 1413 infoText = "" 1414 1415 if show or self.pricesFile: 1416 info = [ 1417 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1418 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1419 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1420 ] 1421 1422 for item in iList: 1423 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1424 item["ticker"], 1425 item["figi"], 1426 item["type"], 1427 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1428 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1429 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1430 "{} / {}".format( 1431 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1432 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1433 ), 1434 "{} / {}".format( 1435 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1436 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1437 ), 1438 item["currency"], 1439 )) 1440 1441 infoText = "".join(info) 1442 1443 if show: 1444 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1445 1446 if self.pricesFile: 1447 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1448 fH.write(infoText) 1449 1450 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1451 1452 return infoText 1453 1454 def RequestTradingStatus(self) -> dict: 1455 """ 1456 Requesting trading status for the instrument defined by `figi` variable. 1457 1458 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1459 1460 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1461 1462 :return: dictionary with trading status attributes. Response example: 1463 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1464 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1465 """ 1466 if self.figi is None or not self.figi: 1467 uLogger.error("Variable `figi` must be defined for using this method!") 1468 raise Exception("FIGI required") 1469 1470 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1471 1472 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1473 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1474 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1475 1476 if self.moreDebug: 1477 uLogger.debug("Records about current trading status successfully received") 1478 1479 return tradingStatus 1480 1481 def RequestPortfolio(self) -> dict: 1482 """ 1483 Requesting actual user's portfolio for current `accountId`. 1484 1485 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1486 1487 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1488 1489 :return: dictionary with user's portfolio. 1490 """ 1491 if self.accountId is None or not self.accountId: 1492 uLogger.error("Variable `accountId` must be defined for using this method!") 1493 raise Exception("Account ID required") 1494 1495 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1496 1497 self.body = str({"accountId": self.accountId}) 1498 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1499 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1500 1501 if self.moreDebug: 1502 uLogger.debug("Records about user's portfolio successfully received") 1503 1504 return rawPortfolio 1505 1506 def RequestPositions(self) -> dict: 1507 """ 1508 Requesting open positions by currencies and instruments for current `accountId`. 1509 1510 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1511 1512 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1513 1514 :return: dictionary with open positions by instruments. 1515 """ 1516 if self.accountId is None or not self.accountId: 1517 uLogger.error("Variable `accountId` must be defined for using this method!") 1518 raise Exception("Account ID required") 1519 1520 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1521 1522 self.body = str({"accountId": self.accountId}) 1523 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1524 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1525 1526 if self.moreDebug: 1527 uLogger.debug("Records about current open positions successfully received") 1528 1529 return rawPositions 1530 1531 def RequestPendingOrders(self) -> list: 1532 """ 1533 Requesting current actual pending orders for current `accountId`. 1534 1535 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1536 1537 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1538 1539 :return: list of dictionaries with pending orders. 1540 """ 1541 if self.accountId is None or not self.accountId: 1542 uLogger.error("Variable `accountId` must be defined for using this method!") 1543 raise Exception("Account ID required") 1544 1545 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1546 1547 self.body = str({"accountId": self.accountId}) 1548 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1549 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1550 1551 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1552 1553 return rawOrders 1554 1555 def RequestStopOrders(self) -> list: 1556 """ 1557 Requesting current actual stop orders for current `accountId`. 1558 1559 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1560 1561 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1562 1563 :return: list of dictionaries with stop orders. 1564 """ 1565 if self.accountId is None or not self.accountId: 1566 uLogger.error("Variable `accountId` must be defined for using this method!") 1567 raise Exception("Account ID required") 1568 1569 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1570 1571 self.body = str({"accountId": self.accountId}) 1572 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1573 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1574 1575 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1576 1577 return rawStopOrders 1578 1579 def Overview(self, show: bool = False, details: str = "full") -> dict: 1580 """ 1581 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1582 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1583 and `overviewBondsCalendarFile` are defined then also save information to file. 1584 1585 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1586 many requests about the state of the portfolio, and then, based on the received data, a large number 1587 of calculation and statistics are collected. 1588 1589 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1590 :param details: how detailed should the information be? 1591 - `full` — shows full available information about portfolio status (by default), 1592 - `positions` — shows only open positions, 1593 - `orders` — shows only sections of open limits and stop orders. 1594 - `digest` — show a short digest of the portfolio status, 1595 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1596 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1597 :return: dictionary with client's raw portfolio and some statistics. 1598 """ 1599 if self.accountId is None or not self.accountId: 1600 uLogger.error("Variable `accountId` must be defined for using this method!") 1601 raise Exception("Account ID required") 1602 1603 view = { 1604 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1605 "headers": {}, # list of dictionaries, response headers without "positions" section 1606 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1607 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1608 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1609 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1610 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1611 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1612 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1613 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1614 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1615 }, 1616 "stat": { # --- some statistics calculated using "raw" sections: 1617 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1618 "availableRUB": 0., # available rubles (without other currencies) 1619 "blockedRUB": 0., # blocked sum in Russian Rouble 1620 "totalChangesRUB": 0., # changes for all open trades in RUB 1621 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1622 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1623 "sharesCostRUB": 0., # costs of all shares in RUB 1624 "bondsCostRUB": 0., # costs of all bonds in RUB 1625 "etfsCostRUB": 0., # costs of all etfs in RUB 1626 "futuresCostRUB": 0., # costs of all futures in RUB 1627 "Currencies": [], # list of dictionaries of all currencies statistics 1628 "Shares": [], # list of dictionaries of all shares statistics 1629 "Bonds": [], # list of dictionaries of all bonds statistics 1630 "Etfs": [], # list of dictionaries of all etfs statistics 1631 "Futures": [], # list of dictionaries of all futures statistics 1632 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1633 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1634 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1635 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1636 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1637 }, 1638 "analytics": { # --- some analytics of portfolio: 1639 "distrByAssets": {}, # portfolio distribution by assets 1640 "distrByCompanies": {}, # portfolio distribution by companies 1641 "distrBySectors": {}, # portfolio distribution by sectors 1642 "distrByCurrencies": {}, # portfolio distribution by currencies 1643 "distrByCountries": {}, # portfolio distribution by countries 1644 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1645 } 1646 } 1647 1648 details = details.lower() 1649 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1650 if details not in availableDetails: 1651 details = "full" 1652 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1653 1654 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1655 1656 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1657 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1658 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1659 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1660 1661 # save response headers without "positions" section: 1662 for key in portfolioResponse.keys(): 1663 if key != "positions": 1664 view["raw"]["headers"][key] = portfolioResponse[key] 1665 1666 else: 1667 continue 1668 1669 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1670 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1671 for item in portfolioResponse["positions"]: 1672 if item["instrumentType"] == "currency": 1673 self.figi = item["figi"] 1674 curr = self.SearchByFIGI(requestPrice=False) 1675 1676 # current price of currency in RUB: 1677 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1678 "name": curr["name"], 1679 "currentPrice": NanoToFloat( 1680 item["currentPrice"]["units"], 1681 item["currentPrice"]["nano"] 1682 ), 1683 } 1684 1685 view["raw"]["Currencies"].append(item) 1686 1687 elif item["instrumentType"] == "share": 1688 view["raw"]["Shares"].append(item) 1689 1690 elif item["instrumentType"] == "bond": 1691 view["raw"]["Bonds"].append(item) 1692 1693 elif item["instrumentType"] == "etf": 1694 view["raw"]["Etfs"].append(item) 1695 1696 elif item["instrumentType"] == "futures": 1697 view["raw"]["Futures"].append(item) 1698 1699 else: 1700 continue 1701 1702 # how many volume of currencies (by ISO currency name) are blocked: 1703 for item in view["raw"]["positions"]["blocked"]: 1704 blocked = NanoToFloat(item["units"], item["nano"]) 1705 if blocked > 0: 1706 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1707 1708 # how many volume of instruments (by FIGI) are blocked: 1709 for item in view["raw"]["positions"]["securities"]: 1710 blocked = int(item["blocked"]) 1711 if blocked > 0: 1712 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1713 1714 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1715 1716 if "rub" in allBlocked.keys(): 1717 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1718 1719 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1720 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1721 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1722 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1723 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1724 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1725 view["stat"]["portfolioCostRUB"] = sum([ 1726 view["stat"]["allCurrenciesCostRUB"], 1727 view["stat"]["sharesCostRUB"], 1728 view["stat"]["bondsCostRUB"], 1729 view["stat"]["etfsCostRUB"], 1730 view["stat"]["futuresCostRUB"], 1731 ]) 1732 1733 # --- calculating some portfolio statistics: 1734 byComp = {} # distribution by companies 1735 bySect = {} # distribution by sectors 1736 byCurr = {} # distribution by currencies (include RUB) 1737 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1738 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1739 1740 for item in portfolioResponse["positions"]: 1741 self.figi = item["figi"] 1742 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1743 1744 if instrument: 1745 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1746 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1747 1748 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1749 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1750 1751 else: 1752 blocked = 0 1753 1754 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1755 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1756 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1757 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1758 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1759 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1760 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1761 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1762 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1763 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1764 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1765 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1766 1767 statData = { 1768 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1769 "ticker": instrument["ticker"], # ticker by FIGI 1770 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1771 "volume": volume, # available volume of instrument 1772 "lots": lots, # volume in lots of instrument 1773 "direction": direction, # direction of an instrument's position: short or long 1774 "blocked": blocked, # blocked volume of currency or instrument 1775 "currentPrice": curPrice, # current instrument's price in basic asset 1776 "average": average, # current average position price 1777 "cost": cost, # current cost of all volume of instrument in basic asset 1778 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1779 "costRUB": costRUB, # cost of instrument in ruble 1780 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1781 "profit": profit, # expected profit at current moment 1782 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1783 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1784 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1785 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1786 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1787 "step": instrument["step"], # minimum price increment 1788 } 1789 1790 # adding distribution by unique countries: 1791 if statData["country"] not in byCountry.keys(): 1792 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1793 1794 else: 1795 byCountry[statData["country"]]["cost"] += costRUB 1796 byCountry[statData["country"]]["percent"] += percentCostRUB 1797 1798 if item["instrumentType"] != "currency": 1799 # adding distribution by unique companies: 1800 if statData["name"]: 1801 if statData["name"] not in byComp.keys(): 1802 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1803 1804 else: 1805 byComp[statData["name"]]["cost"] += costRUB 1806 byComp[statData["name"]]["percent"] += percentCostRUB 1807 1808 # adding distribution by unique sectors: 1809 if statData["sector"] not in bySect.keys(): 1810 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1811 1812 else: 1813 bySect[statData["sector"]]["cost"] += costRUB 1814 bySect[statData["sector"]]["percent"] += percentCostRUB 1815 1816 # adding distribution by unique currencies: 1817 if currency not in byCurr.keys(): 1818 byCurr[currency] = { 1819 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1820 "cost": costRUB, 1821 "percent": percentCostRUB 1822 } 1823 1824 else: 1825 byCurr[currency]["cost"] += costRUB 1826 byCurr[currency]["percent"] += percentCostRUB 1827 1828 # saving statistics for every instrument: 1829 if item["instrumentType"] == "currency": 1830 view["stat"]["Currencies"].append(statData) 1831 1832 # update dict with free funds for trading (total - blocked) by currencies 1833 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1834 view["stat"]["funds"][currency] = { 1835 "total": volume, 1836 "totalCostRUB": costRUB, # total volume cost in rubles 1837 "free": volume - blocked, 1838 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1839 } 1840 1841 elif item["instrumentType"] == "share": 1842 view["stat"]["Shares"].append(statData) 1843 1844 elif item["instrumentType"] == "bond": 1845 view["stat"]["Bonds"].append(statData) 1846 1847 elif item["instrumentType"] == "etf": 1848 view["stat"]["Etfs"].append(statData) 1849 1850 elif item["instrumentType"] == "Futures": 1851 view["stat"]["Futures"].append(statData) 1852 1853 else: 1854 continue 1855 1856 # total changes in Russian Ruble: 1857 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1858 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1859 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1860 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1861 view["stat"]["funds"]["rub"] = { 1862 "total": view["stat"]["availableRUB"], 1863 "totalCostRUB": view["stat"]["availableRUB"], 1864 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1865 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1866 } 1867 1868 # --- pending orders sector data: 1869 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending orders to avoid many times price requests 1870 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1871 1872 for item in view["raw"]["orders"]: 1873 self.figi = item["figi"] 1874 1875 if item["figi"] not in uniquePendingOrdersFIGIs: 1876 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1877 1878 uniquePendingOrdersFIGIs.append(item["figi"]) 1879 uniquePendingOrders[item["figi"]] = instrument 1880 1881 else: 1882 instrument = uniquePendingOrders[item["figi"]] 1883 1884 if instrument: 1885 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1886 orderType = TKS_ORDER_TYPES[item["orderType"]] 1887 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1888 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1889 1890 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1891 if item["direction"] == "ORDER_DIRECTION_BUY": 1892 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1893 1894 else: 1895 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1896 1897 # requested price for order execution: 1898 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1899 1900 # necessary changes in percent to reach target from current price: 1901 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1902 1903 view["stat"]["orders"].append({ 1904 "orderID": item["orderId"], # orderId number parameter of current order 1905 "figi": item["figi"], # FIGI identification 1906 "ticker": instrument["ticker"], # ticker name by FIGI 1907 "lotsRequested": item["lotsRequested"], # requested lots value 1908 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1909 "currentPrice": lastPrice, # current instrument's price for defined action 1910 "targetPrice": target, # requested price for order execution in base currency 1911 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1912 "percentChanges": changes, # changes in percent to target from current price 1913 "currency": item["currency"], # instrument's currency name 1914 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1915 "type": orderType, # type of order from TKS_ORDER_TYPES 1916 "status": orderState, # order status from TKS_ORDER_STATES 1917 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1918 }) 1919 1920 # --- stop orders sector data: 1921 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1922 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1923 1924 for item in view["raw"]["stopOrders"]: 1925 self.figi = item["figi"] 1926 1927 if item["figi"] not in uniqueStopOrdersFIGIs: 1928 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1929 1930 uniqueStopOrdersFIGIs.append(item["figi"]) 1931 uniqueStopOrders[item["figi"]] = instrument 1932 1933 else: 1934 instrument = uniqueStopOrders[item["figi"]] 1935 1936 if instrument: 1937 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1938 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1939 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1940 1941 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1942 if "expirationTime" in item.keys(): 1943 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1944 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1945 1946 else: 1947 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1948 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1949 1950 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1951 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1952 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1953 1954 else: 1955 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1956 1957 # requested price when stop-order executed: 1958 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1959 1960 # price for limit-order, set up when stop-order executed: 1961 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1962 1963 # necessary changes in percent to reach target from current price: 1964 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1965 1966 view["stat"]["stopOrders"].append({ 1967 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1968 "figi": item["figi"], # FIGI identification 1969 "ticker": instrument["ticker"], # ticker name by FIGI 1970 "lotsRequested": item["lotsRequested"], # requested lots value 1971 "currentPrice": lastPrice, # current instrument's price for defined action 1972 "targetPrice": target, # requested price for stop-order execution in base currency 1973 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1974 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1975 "percentChanges": changes, # changes in percent to target from current price 1976 "currency": item["currency"], # instrument's currency name 1977 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1978 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1979 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1980 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1981 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 1982 }) 1983 1984 # --- calculating data for analytics section: 1985 # portfolio distribution by assets: 1986 view["analytics"]["distrByAssets"] = { 1987 "Ruble": { 1988 "uniques": 1, 1989 "cost": view["stat"]["availableRUB"], 1990 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1991 }, 1992 "Currencies": { 1993 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 1994 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 1995 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1996 }, 1997 "Shares": { 1998 "uniques": len(view["stat"]["Shares"]), 1999 "cost": view["stat"]["sharesCostRUB"], 2000 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2001 }, 2002 "Bonds": { 2003 "uniques": len(view["stat"]["Bonds"]), 2004 "cost": view["stat"]["bondsCostRUB"], 2005 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2006 }, 2007 "Etfs": { 2008 "uniques": len(view["stat"]["Etfs"]), 2009 "cost": view["stat"]["etfsCostRUB"], 2010 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2011 }, 2012 "Futures": { 2013 "uniques": len(view["stat"]["Futures"]), 2014 "cost": view["stat"]["futuresCostRUB"], 2015 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2016 }, 2017 } 2018 2019 # portfolio distribution by companies: 2020 view["analytics"]["distrByCompanies"]["All money cash"] = { 2021 "ticker": "", 2022 "cost": view["stat"]["allCurrenciesCostRUB"], 2023 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2024 } 2025 view["analytics"]["distrByCompanies"].update(byComp) 2026 2027 # portfolio distribution by sectors: 2028 view["analytics"]["distrBySectors"]["All money cash"] = { 2029 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2030 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2031 } 2032 view["analytics"]["distrBySectors"].update(bySect) 2033 2034 # portfolio distribution by currencies: 2035 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2036 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2037 2038 if self.moreDebug: 2039 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2040 2041 view["analytics"]["distrByCurrencies"].update(byCurr) 2042 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2043 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2044 2045 # portfolio distribution by countries: 2046 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2047 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2048 2049 if self.moreDebug: 2050 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2051 2052 view["analytics"]["distrByCountries"].update(byCountry) 2053 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2054 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2055 2056 # --- Prepare text statistics overview in human-readable: 2057 if show: 2058 # Whatever the value `details`, header not changes: 2059 info = [ 2060 "# Client's portfolio\n\n", 2061 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2062 "* **Account ID:** [{}]\n".format(self.accountId), 2063 ] 2064 2065 if details in ["full", "positions", "digest"]: 2066 info.extend([ 2067 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2068 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2069 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2070 view["stat"]["totalChangesRUB"], 2071 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2072 view["stat"]["totalChangesPercentRUB"], 2073 ), 2074 ]) 2075 2076 if details in ["full", "positions"]: 2077 info.extend([ 2078 "## Open positions\n\n", 2079 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2080 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2081 "| Ruble | {:>31} | | | | | |\n".format( 2082 "{:.2f} ({:.2f}) rub".format( 2083 view["stat"]["availableRUB"], 2084 view["stat"]["blockedRUB"], 2085 ) 2086 ) 2087 ]) 2088 2089 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2090 return [ 2091 "| | | | | | | |\n", 2092 "| {:<27} | | | | | {:>19} | |\n".format( 2093 noTradeStr if noTradeStr else typeStr, 2094 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2095 ), 2096 ] 2097 2098 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2099 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2100 "{} [{}]".format(data["ticker"], data["figi"]), 2101 "{:.2f} ({:.2f}) {}".format( 2102 data["volume"], 2103 data["blocked"], 2104 data["currency"], 2105 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2106 data["volume"], 2107 data["blocked"], 2108 ), 2109 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2110 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2111 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2112 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2113 "{}{:.2f} {} ({}{:.2f}%)".format( 2114 "+" if data["profit"] > 0 else "", 2115 data["profit"], data["baseCurrencyName"], 2116 "+" if data["percentProfit"] > 0 else "", 2117 data["percentProfit"], 2118 ), 2119 ) 2120 2121 # --- Show currencies section: 2122 if view["stat"]["Currencies"]: 2123 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2124 for item in view["stat"]["Currencies"]: 2125 info.append(_InfoStr(item, showCurrencyName=True)) 2126 2127 else: 2128 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2129 2130 # --- Show shares section: 2131 if view["stat"]["Shares"]: 2132 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2133 2134 for item in view["stat"]["Shares"]: 2135 info.append(_InfoStr(item)) 2136 2137 else: 2138 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2139 2140 # --- Show bonds section: 2141 if view["stat"]["Bonds"]: 2142 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2143 2144 for item in view["stat"]["Bonds"]: 2145 info.append(_InfoStr(item)) 2146 2147 else: 2148 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2149 2150 # --- Show etfs section: 2151 if view["stat"]["Etfs"]: 2152 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2153 2154 for item in view["stat"]["Etfs"]: 2155 info.append(_InfoStr(item)) 2156 2157 else: 2158 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2159 2160 # --- Show futures section: 2161 if view["stat"]["Futures"]: 2162 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2163 2164 for item in view["stat"]["Futures"]: 2165 info.append(_InfoStr(item)) 2166 2167 else: 2168 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2169 2170 if details in ["full", "orders"]: 2171 # --- Show pending orders section: 2172 if view["stat"]["orders"]: 2173 info.extend([ 2174 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2175 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2176 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2177 ]) 2178 2179 for item in view["stat"]["orders"]: 2180 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2181 "{} [{}]".format(item["ticker"], item["figi"]), 2182 item["orderID"], 2183 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2184 "{} {} ({}{:.2f}%)".format( 2185 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2186 item["baseCurrencyName"], 2187 "+" if item["percentChanges"] > 0 else "", 2188 float(item["percentChanges"]), 2189 ), 2190 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2191 item["action"], 2192 item["type"], 2193 item["date"], 2194 )) 2195 2196 else: 2197 info.append("\n## Total pending limit-orders: 0\n") 2198 2199 # --- Show stop orders section: 2200 if view["stat"]["stopOrders"]: 2201 info.extend([ 2202 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2203 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2204 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2205 ]) 2206 2207 for item in view["stat"]["stopOrders"]: 2208 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2209 "{} [{}]".format(item["ticker"], item["figi"]), 2210 item["orderID"], 2211 item["lotsRequested"], 2212 "{} {} ({}{:.2f}%)".format( 2213 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2214 item["baseCurrencyName"], 2215 "+" if item["percentChanges"] > 0 else "", 2216 float(item["percentChanges"]), 2217 ), 2218 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2219 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2220 item["action"], 2221 item["type"], 2222 item["expType"], 2223 item["createDate"], 2224 item["expDate"], 2225 )) 2226 2227 else: 2228 info.append("\n## Total stop-orders: 0\n") 2229 2230 if details in ["full", "analytics"]: 2231 # -- Show analytics section: 2232 if view["stat"]["portfolioCostRUB"] > 0: 2233 info.extend([ 2234 "\n# Analytics\n" 2235 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2236 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2237 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2238 view["stat"]["totalChangesRUB"], 2239 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2240 view["stat"]["totalChangesPercentRUB"], 2241 ), 2242 "\n## Portfolio distribution by assets\n" 2243 "\n| Type | Uniques | Percent | Current cost |\n", 2244 "|------------------------------------|---------|---------|--------------------|\n", 2245 ]) 2246 2247 for key in view["analytics"]["distrByAssets"].keys(): 2248 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2249 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2250 key, 2251 view["analytics"]["distrByAssets"][key]["uniques"], 2252 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2253 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2254 )) 2255 2256 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2257 2258 info.extend([ 2259 "\n## Portfolio distribution by companies\n" 2260 "\n| Company | Percent | Current cost |\n", 2261 aSepLine, 2262 ]) 2263 2264 for company in view["analytics"]["distrByCompanies"].keys(): 2265 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2266 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2267 "{}{}".format( 2268 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2269 company, 2270 ), 2271 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2272 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2273 )) 2274 2275 info.extend([ 2276 "\n## Portfolio distribution by sectors\n" 2277 "\n| Sector | Percent | Current cost |\n", 2278 aSepLine, 2279 ]) 2280 2281 for sector in view["analytics"]["distrBySectors"].keys(): 2282 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2283 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2284 sector, 2285 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2286 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2287 )) 2288 2289 info.extend([ 2290 "\n## Portfolio distribution by currencies\n" 2291 "\n| Instruments currencies | Percent | Current cost |\n", 2292 aSepLine, 2293 ]) 2294 2295 for curr in view["analytics"]["distrByCurrencies"].keys(): 2296 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2297 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2298 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2299 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2300 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2301 )) 2302 2303 info.extend([ 2304 "\n## Portfolio distribution by countries\n" 2305 "\n| Assets by country | Percent | Current cost |\n", 2306 aSepLine, 2307 ]) 2308 2309 for country in view["analytics"]["distrByCountries"].keys(): 2310 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2311 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2312 country, 2313 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2314 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2315 )) 2316 2317 if details in ["full", "calendar"]: 2318 # -- Show bonds payment calendar section: 2319 if view["stat"]["Bonds"]: 2320 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2321 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2322 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2323 2324 else: 2325 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2326 2327 infoText = "".join(info) 2328 2329 uLogger.info(infoText) 2330 2331 if details == "full" and self.overviewFile: 2332 filename = self.overviewFile 2333 2334 elif details == "digest" and self.overviewDigestFile: 2335 filename = self.overviewDigestFile 2336 2337 elif details == "positions" and self.overviewPositionsFile: 2338 filename = self.overviewPositionsFile 2339 2340 elif details == "orders" and self.overviewOrdersFile: 2341 filename = self.overviewOrdersFile 2342 2343 elif details == "analytics" and self.overviewAnalyticsFile: 2344 filename = self.overviewAnalyticsFile 2345 2346 elif details == "calendar" and self.overviewBondsCalendarFile: 2347 filename = self.overviewBondsCalendarFile 2348 2349 else: 2350 filename = "" 2351 2352 if filename: 2353 with open(filename, "w", encoding="UTF-8") as fH: 2354 fH.write(infoText) 2355 2356 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2357 2358 return view 2359 2360 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2361 """ 2362 Returns history operations between two given dates for current `accountId`. 2363 If `reportFile` string is not empty then also save human-readable report. 2364 Shows some statistical data of closed positions. 2365 2366 :param start: see docstring in `GetDatesAsString()` method 2367 :param end: see docstring in `GetDatesAsString()` method 2368 :param show: if `True` then also prints all records to the console. 2369 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2370 :return: original list of dictionaries with history of deals records from API ("operations" key): 2371 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2372 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2373 """ 2374 if self.accountId is None or not self.accountId: 2375 uLogger.error("Variable `accountId` must be defined for using this method!") 2376 raise Exception("Account ID required") 2377 2378 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2379 2380 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2381 2382 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2383 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2384 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2385 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2386 customStat = {} # custom statistics in additional to responseJSON 2387 2388 # --- output report in human-readable format: 2389 if show or self.reportFile: 2390 splitLine1 = "| | | | | |\n" # Summary section 2391 splitLine2 = "| | | | | | | | |\n" # Operations section 2392 nextDay = "" 2393 2394 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2395 2396 if len(ops) > 0: 2397 customStat = { 2398 "opsCount": 0, # total operations count 2399 "buyCount": 0, # buy operations 2400 "sellCount": 0, # sell operations 2401 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2402 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2403 "payIn": {"rub": 0.}, # Deposit brokerage account 2404 "payOut": {"rub": 0.}, # Withdrawals 2405 "divs": {"rub": 0.}, # Dividends income 2406 "coupons": {"rub": 0.}, # Coupon's income 2407 "brokerCom": {"rub": 0.}, # Service commissions 2408 "serviceCom": {"rub": 0.}, # Service commissions 2409 "marginCom": {"rub": 0.}, # Margin commissions 2410 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2411 } 2412 2413 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2414 for item in ops: 2415 if item["state"] == "OPERATION_STATE_EXECUTED": 2416 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2417 2418 # count buy operations: 2419 if "_BUY" in item["operationType"]: 2420 customStat["buyCount"] += 1 2421 2422 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2423 customStat["buyTotal"][item["payment"]["currency"]] += payment 2424 2425 else: 2426 customStat["buyTotal"][item["payment"]["currency"]] = payment 2427 2428 # count sell operations: 2429 elif "_SELL" in item["operationType"]: 2430 customStat["sellCount"] += 1 2431 2432 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2433 customStat["sellTotal"][item["payment"]["currency"]] += payment 2434 2435 else: 2436 customStat["sellTotal"][item["payment"]["currency"]] = payment 2437 2438 # count incoming operations: 2439 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2440 if item["payment"]["currency"] in customStat["payIn"].keys(): 2441 customStat["payIn"][item["payment"]["currency"]] += payment 2442 2443 else: 2444 customStat["payIn"][item["payment"]["currency"]] = payment 2445 2446 # count withdrawals operations: 2447 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2448 if item["payment"]["currency"] in customStat["payOut"].keys(): 2449 customStat["payOut"][item["payment"]["currency"]] += payment 2450 2451 else: 2452 customStat["payOut"][item["payment"]["currency"]] = payment 2453 2454 # count dividends income: 2455 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2456 if item["payment"]["currency"] in customStat["divs"].keys(): 2457 customStat["divs"][item["payment"]["currency"]] += payment 2458 2459 else: 2460 customStat["divs"][item["payment"]["currency"]] = payment 2461 2462 # count coupon's income: 2463 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2464 if item["payment"]["currency"] in customStat["coupons"].keys(): 2465 customStat["coupons"][item["payment"]["currency"]] += payment 2466 2467 else: 2468 customStat["coupons"][item["payment"]["currency"]] = payment 2469 2470 # count broker commissions: 2471 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2472 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2473 customStat["brokerCom"][item["payment"]["currency"]] += payment 2474 2475 else: 2476 customStat["brokerCom"][item["payment"]["currency"]] = payment 2477 2478 # count service commissions: 2479 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2480 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2481 customStat["serviceCom"][item["payment"]["currency"]] += payment 2482 2483 else: 2484 customStat["serviceCom"][item["payment"]["currency"]] = payment 2485 2486 # count margin commissions: 2487 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2488 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2489 customStat["marginCom"][item["payment"]["currency"]] += payment 2490 2491 else: 2492 customStat["marginCom"][item["payment"]["currency"]] = payment 2493 2494 # count withholding taxes: 2495 elif "_TAX" in item["operationType"]: 2496 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2497 customStat["allTaxes"][item["payment"]["currency"]] += payment 2498 2499 else: 2500 customStat["allTaxes"][item["payment"]["currency"]] = payment 2501 2502 else: 2503 continue 2504 2505 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2506 2507 # --- view "Actions" lines: 2508 info.extend([ 2509 "| Report sections | | | | |\n", 2510 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2511 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2512 "| | Buy: {:<22} | {:<28} | | |\n".format( 2513 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2514 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2515 ), 2516 "| | Sell: {:<21} | {:<28} | | |\n".format( 2517 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2518 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2519 ), 2520 ]) 2521 2522 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2523 for key in opsKeys: 2524 if key == "rub": 2525 continue 2526 2527 info.extend([ 2528 "| | | {:<28} | | |\n".format( 2529 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2530 ), 2531 "| | | {:<28} | | |\n".format( 2532 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2533 ), 2534 ]) 2535 2536 info.append(splitLine1) 2537 2538 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2539 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2540 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2541 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2542 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2543 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2544 ) 2545 2546 # --- view "Payments" lines: 2547 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2548 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2549 2550 for key in paymentsKeys: 2551 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2552 2553 info.append(splitLine1) 2554 2555 # --- view "Commissions and taxes" lines: 2556 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2557 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2558 2559 for key in comKeys: 2560 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2561 2562 info.append(splitLine1) 2563 2564 info.extend([ 2565 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2566 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2567 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2568 ]) 2569 2570 else: 2571 info.append("Broker returned no operations during this period\n") 2572 2573 # --- view "Operations" section: 2574 for item in ops: 2575 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2576 continue 2577 2578 else: 2579 self.figi = item["figi"] if item["figi"] else "" 2580 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2581 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2582 2583 # group of deals during one day: 2584 if nextDay and item["date"].split("T")[0] != nextDay: 2585 info.append(splitLine2) 2586 nextDay = "" 2587 2588 else: 2589 nextDay = item["date"].split("T")[0] # saving current day for splitting 2590 2591 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2592 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2593 self.figi if self.figi else "—", 2594 instrument["ticker"] if instrument else "—", 2595 instrument["type"] if instrument else "—", 2596 item["quantity"] if int(item["quantity"]) > 0 else "—", 2597 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2598 TKS_OPERATION_STATES[item["state"]], 2599 TKS_OPERATION_TYPES[item["operationType"]], 2600 )) 2601 2602 infoText = "".join(info) 2603 2604 if show: 2605 if self.moreDebug: 2606 uLogger.debug("Records about history of a client's operations successfully received") 2607 2608 uLogger.info(infoText) 2609 2610 if self.reportFile: 2611 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2612 fH.write(infoText) 2613 2614 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2615 2616 return ops, customStat 2617 2618 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2619 """ 2620 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2621 2622 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2623 Warning! Broker server used ISO UTC time by default. 2624 2625 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2626 Also, `historyFile` used to update history with `onlyMissing` parameter. 2627 2628 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2629 2630 :param start: see docstring in `GetDatesAsString()` method. 2631 :param end: see docstring in `GetDatesAsString()` method. 2632 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2633 `"hour"`, `"day"`. Default: `"hour"`. 2634 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2635 False by default. Warning! History appends only from last candle to current time 2636 with always update last candle! 2637 :param csvSep: separator if csv-file is used, `,` by default. 2638 :param show: if `True` then also prints Pandas DataFrame to the console. 2639 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2640 `["date", "time", "open", "high", "low", "close", "volume"]`. 2641 """ 2642 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2643 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2644 history = None # empty pandas object for history 2645 2646 if interval not in TKS_CANDLE_INTERVALS.keys(): 2647 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2648 raise Exception("Incorrect value") 2649 2650 if not (self.ticker or self.figi): 2651 uLogger.error("Ticker or FIGI must be defined!") 2652 raise Exception("Ticker or FIGI required") 2653 2654 if self.ticker and not self.figi: 2655 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2656 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2657 2658 if self.figi and not self.ticker: 2659 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2660 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2661 2662 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2663 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2664 if interval.lower() != "day": 2665 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2666 2667 delta = dtEnd - dtStart # current UTC time minus last time in file 2668 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2669 2670 # calculate history length in candles: 2671 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2672 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2673 length += 1 # to avoid fraction time 2674 2675 # calculate data blocks count: 2676 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2677 2678 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2679 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2680 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2681 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2682 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2683 2684 tempOld = None # pandas object for old history, if --only-missing key present 2685 lastTime = None # datetime object of last old candle in file 2686 2687 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2688 uLogger.debug("--only-missing key present, add only last missing candles...") 2689 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2690 2691 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2692 2693 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2694 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2695 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2696 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2697 2698 # get last datetime object from last string in file or minus 1 delta if file is empty: 2699 if len(tempOld) > 0: 2700 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2701 2702 else: 2703 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2704 2705 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2706 2707 responseJSONs = [] # raw history blocks of data 2708 2709 blockEnd = dtEnd 2710 for item in range(blocks): 2711 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2712 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2713 2714 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2715 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2716 )) 2717 2718 if blockStart == blockEnd: 2719 uLogger.debug("Skipped this zero-length block...") 2720 2721 else: 2722 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2723 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2724 self.body = str({ 2725 "figi": self.figi, 2726 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2727 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2728 "interval": TKS_CANDLE_INTERVALS[interval][0] 2729 }) 2730 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2731 2732 if "code" in responseJSON.keys(): 2733 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2734 2735 else: 2736 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2737 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2738 2739 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2740 2741 blockEnd = blockStart 2742 2743 printCount = len(responseJSONs) # candles to show in console 2744 if responseJSONs: 2745 tempHistory = pd.DataFrame( 2746 data={ 2747 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2748 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2749 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2750 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2751 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2752 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2753 "volume": [int(item["volume"]) for item in responseJSONs], 2754 }, 2755 index=range(len(responseJSONs)), 2756 columns=["date", "time", "open", "high", "low", "close", "volume"], 2757 ) 2758 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2759 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2760 2761 # append only newest candles to old history if --only-missing key present: 2762 if onlyMissing and tempOld is not None and lastTime is not None: 2763 index = 0 # find start index in tempHistory data: 2764 2765 for i, item in tempHistory.iterrows(): 2766 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2767 2768 if curTime == lastTime: 2769 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2770 index = i 2771 printCount = index + 1 2772 break 2773 2774 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2775 2776 else: 2777 history = tempHistory # if no `--only-missing` key then load full data from server 2778 2779 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2780 2781 if history is not None and not history.empty: 2782 if show: 2783 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2784 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2785 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2786 )) 2787 2788 else: 2789 uLogger.warning("Received an empty candles history!") 2790 2791 if self.historyFile is not None: 2792 if history is not None and not history.empty: 2793 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2794 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2795 2796 else: 2797 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2798 2799 else: 2800 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2801 2802 return history 2803 2804 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2805 """ 2806 Load candles history from csv-file and return Pandas DataFrame object. 2807 2808 See also: `History()` and `ShowHistoryChart()` methods. 2809 2810 :param filePath: path to csv-file to open. 2811 """ 2812 loadedHistory = None # init candles data object 2813 2814 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2815 2816 if os.path.exists(filePath): 2817 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2818 2819 tfStr = self.priceModel.FormattedDelta( 2820 self.priceModel.timeframe, 2821 "{days} days {hours}h {minutes}m {seconds}s", 2822 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2823 self.priceModel.timeframe, 2824 "{hours}h {minutes}m {seconds}s", 2825 ) 2826 2827 if loadedHistory is not None and not loadedHistory.empty: 2828 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2829 len(loadedHistory), 2830 tfStr, 2831 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2832 ) 2833 2834 else: 2835 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2836 2837 else: 2838 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2839 2840 return loadedHistory 2841 2842 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2843 """ 2844 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2845 2846 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2847 Default: `index.html` (both for interact and non-interact candlesticks chart). 2848 2849 See also: `History()` and `LoadHistory()` methods. 2850 2851 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2852 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2853 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2854 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2855 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2856 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2857 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2858 """ 2859 if isinstance(candles, str): 2860 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2861 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2862 2863 elif isinstance(candles, pd.DataFrame): 2864 self.priceModel.prices = candles # set candles chain from variable 2865 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2866 2867 if "datetime" not in candles.columns: 2868 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2869 2870 else: 2871 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2872 raise Exception("Incorrect value") 2873 2874 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2875 2876 if interact: 2877 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2878 2879 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2880 2881 else: 2882 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2883 2884 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2885 2886 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2887 2888 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2889 """ 2890 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2891 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2892 2893 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2894 2895 :param operation: string "Buy" or "Sell". 2896 :param lots: volume, integer count of lots >= 1. 2897 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2898 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2899 :param expDate: string "Undefined" by default or local date in future, 2900 it is a string with format `%Y-%m-%d %H:%M:%S`. 2901 :return: JSON with response from broker server. 2902 """ 2903 if self.accountId is None or not self.accountId: 2904 uLogger.error("Variable `accountId` must be defined for using this method!") 2905 raise Exception("Account ID required") 2906 2907 if operation is None or not operation or operation not in ("Buy", "Sell"): 2908 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2909 raise Exception("Incorrect value") 2910 2911 if lots is None or lots < 1: 2912 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2913 lots = 1 2914 2915 if tp is None or tp < 0: 2916 tp = 0 2917 2918 if sl is None or sl < 0: 2919 sl = 0 2920 2921 if expDate is None or not expDate: 2922 expDate = "Undefined" 2923 2924 if not (self.ticker or self.figi): 2925 uLogger.error("Ticker or FIGI must be defined!") 2926 raise Exception("Ticker or FIGI required") 2927 2928 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 2929 self.ticker = instrument["ticker"] 2930 self.figi = instrument["figi"] 2931 2932 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2933 2934 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2935 self.body = str({ 2936 "figi": self.figi, 2937 "quantity": str(lots), 2938 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2939 "accountId": str(self.accountId), 2940 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2941 }) 2942 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 2943 2944 if "orderId" in response.keys(): 2945 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2946 operation, response["orderId"], 2947 self.ticker, self.figi, lots, 2948 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2949 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2950 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2951 )) 2952 2953 if tp > 0: 2954 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2955 2956 if sl > 0: 2957 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2958 2959 else: 2960 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.") 2961 2962 return response 2963 2964 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2965 """ 2966 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2967 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2968 2969 See also: `Order()` and `Trade()` docstrings. 2970 2971 :param lots: volume, integer count of lots >= 1. 2972 :param tp: float > 0, take profit price of stop-order. 2973 :param sl: float > 0, stop loss price of stop-order. 2974 :param expDate: it's a local date in future. 2975 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2976 :return: JSON with response from broker server. 2977 """ 2978 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 2979 2980 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2981 """ 2982 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2983 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2984 2985 See also: `Order()` and `Trade()` docstrings. 2986 2987 :param lots: volume, integer count of lots >= 1. 2988 :param tp: float > 0, take profit price of stop-order. 2989 :param sl: float > 0, stop loss price of stop-order. 2990 :param expDate: it's a local date in the future. 2991 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2992 :return: JSON with response from broker server. 2993 """ 2994 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 2995 2996 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 2997 """ 2998 Close position of given instruments. 2999 3000 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3001 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3002 This avoids unnecessary downloading data from the server. 3003 """ 3004 if instruments is None or not instruments: 3005 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3006 raise Exception("Ticker or FIGI required") 3007 3008 if isinstance(instruments, str): 3009 instruments = [instruments] 3010 3011 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3012 if uniqueInstruments: 3013 if portfolio is None or not portfolio: 3014 portfolio = self.Overview(show=False) 3015 3016 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3017 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3018 3019 for self.figi in uniqueInstruments: 3020 if self.figi not in allOpened: 3021 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi)) 3022 continue 3023 3024 # search open trade info about instrument by ticker: 3025 instrument = {} 3026 for iType in TKS_INSTRUMENTS: 3027 if instrument: 3028 break 3029 3030 for item in portfolio["stat"][iType]: 3031 if item["figi"] == self.figi: 3032 instrument = item 3033 break 3034 3035 if instrument: 3036 self.ticker = instrument["ticker"] 3037 self.figi = instrument["figi"] 3038 3039 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3040 self.ticker, 3041 self.figi, 3042 int(instrument["volume"]), 3043 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3044 )) 3045 3046 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3047 3048 if tradeLots > 0: 3049 if instrument["blocked"] > 0: 3050 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3051 instrument["blocked"], 3052 self.ticker, 3053 tradeLots, 3054 )) 3055 3056 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3057 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3058 3059 else: 3060 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker)) 3061 3062 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3063 """ 3064 Close all positions of given instruments with defined type. 3065 3066 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3067 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3068 This avoids unnecessary downloading data from the server. 3069 """ 3070 if iType not in TKS_INSTRUMENTS: 3071 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3072 3073 else: 3074 if portfolio is None or not portfolio: 3075 portfolio = self.Overview(show=False) 3076 3077 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3078 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3079 3080 if tickers and portfolio: 3081 self.CloseTrades(tickers, portfolio) 3082 3083 else: 3084 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3085 3086 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3087 """ 3088 Universal method to create market or limit orders with all available parameters for current `accountId`. 3089 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3090 3091 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3092 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3093 3094 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3095 then broker immediately open market order as you can do simple --buy or --sell operations! 3096 3097 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3098 When current price will go up or down to target price value then broker opens a limit order. 3099 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3100 3101 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3102 3103 :param operation: string "Buy" or "Sell". 3104 :param orderType: string "Limit" or "Stop". 3105 :param lots: volume, integer count of lots >= 1. 3106 :param targetPrice: target price > 0. This is open trade price for limit order. 3107 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3108 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3109 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3110 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3111 Stop loss order always executed by market price. 3112 :param expDate: string "Undefined" by default or local date in future. 3113 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3114 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3115 A limit order has no expiration date, it lasts until the end of the trading day. 3116 :return: JSON with response from broker server. 3117 """ 3118 if self.accountId is None or not self.accountId: 3119 uLogger.error("Variable `accountId` must be defined for using this method!") 3120 raise Exception("Account ID required") 3121 3122 if operation is None or not operation or operation not in ("Buy", "Sell"): 3123 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3124 raise Exception("Incorrect value") 3125 3126 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3127 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3128 raise Exception("Incorrect value") 3129 3130 if lots is None or lots < 1: 3131 uLogger.error("You must define trade volume > 0: integer count of lots!") 3132 raise Exception("Incorrect value") 3133 3134 if targetPrice is None or targetPrice <= 0: 3135 uLogger.error("Target price for limit-order must be greater than 0!") 3136 raise Exception("Incorrect value") 3137 3138 if limitPrice is None or limitPrice <= 0: 3139 limitPrice = targetPrice 3140 3141 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3142 stopType = "Limit" 3143 3144 if expDate is None or not expDate: 3145 expDate = "Undefined" 3146 3147 if not (self.ticker or self.figi): 3148 uLogger.error("Tocker or FIGI must be defined!") 3149 raise Exception("Ticker or FIGI required") 3150 3151 response = {} 3152 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 3153 self.ticker = instrument["ticker"] 3154 self.figi = instrument["figi"] 3155 3156 if orderType == "Limit": 3157 uLogger.debug( 3158 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3159 self.ticker, self.figi, 3160 operation, lots, targetPrice, instrument["currency"], 3161 )) 3162 3163 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3164 self.body = str({ 3165 "figi": self.figi, 3166 "quantity": str(lots), 3167 "price": FloatToNano(targetPrice), 3168 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3169 "accountId": str(self.accountId), 3170 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3171 }) 3172 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3173 3174 if "orderId" in response.keys(): 3175 uLogger.info( 3176 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3177 response["orderId"], 3178 self.ticker, self.figi, 3179 operation, lots, targetPrice, instrument["currency"], 3180 )) 3181 3182 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3183 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3184 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3185 targetPrice, instrument["currency"], 3186 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3187 )) 3188 3189 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3190 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3191 targetPrice, instrument["currency"], 3192 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3193 )) 3194 3195 else: 3196 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3197 3198 if orderType == "Stop": 3199 uLogger.debug( 3200 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3201 self.ticker, self.figi, 3202 operation, lots, 3203 targetPrice, instrument["currency"], 3204 limitPrice, instrument["currency"], 3205 stopType, expDate, 3206 )) 3207 3208 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3209 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3210 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3211 3212 body = { 3213 "figi": self.figi, 3214 "quantity": str(lots), 3215 "price": FloatToNano(limitPrice), 3216 "stopPrice": FloatToNano(targetPrice), 3217 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3218 "accountId": str(self.accountId), 3219 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3220 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3221 } 3222 3223 if expDateUTC: 3224 body["expireDate"] = expDateUTC 3225 3226 self.body = str(body) 3227 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3228 3229 if "stopOrderId" in response.keys(): 3230 uLogger.info( 3231 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3232 response["stopOrderId"], 3233 self.ticker, self.figi, 3234 operation, lots, 3235 targetPrice, instrument["currency"], 3236 limitPrice, instrument["currency"], 3237 TKS_STOP_ORDER_TYPES[stopOrderType], 3238 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3239 )) 3240 3241 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3242 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3243 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3244 targetPrice, instrument["currency"], 3245 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3246 )) 3247 3248 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3249 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3250 targetPrice, instrument["currency"], 3251 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3252 )) 3253 3254 else: 3255 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3256 3257 return response 3258 3259 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3260 """ 3261 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3262 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3263 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3264 See also: `Order()` docstring. 3265 3266 :param lots: volume, integer count of lots >= 1. 3267 :param targetPrice: target price > 0. This is open trade price for limit order. 3268 :return: JSON with response from broker server. 3269 """ 3270 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3271 3272 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3273 """ 3274 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3275 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3276 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3277 target price value then broker opens a limit order. See also: `Order()` docstring. 3278 3279 :param lots: volume, integer count of lots >= 1. 3280 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3281 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3282 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3283 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3284 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3285 :param expDate: string "Undefined" by default or local date in future. 3286 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3287 This date is converting to UTC format for server. 3288 :return: JSON with response from broker server. 3289 """ 3290 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3291 3292 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3293 """ 3294 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3295 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3296 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3297 See also: `Order()` docstring. 3298 3299 :param lots: volume, integer count of lots >= 1. 3300 :param targetPrice: target price > 0. This is open trade price for limit order. 3301 :return: JSON with response from broker server. 3302 """ 3303 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3304 3305 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3306 """ 3307 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3308 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3309 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3310 target price value then broker opens a limit order. See also: `Order()` docstring. 3311 3312 :param lots: volume, integer count of lots >= 1. 3313 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3314 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3315 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3316 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3317 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3318 :param expDate: string "Undefined" by default or local date in future. 3319 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3320 This date is converting to UTC format for server. 3321 :return: JSON with response from broker server. 3322 """ 3323 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3324 3325 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3326 """ 3327 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3328 3329 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3330 :param allOrdersIDs: pre-received lists of all active pending orders. 3331 This avoids unnecessary downloading data from the server. 3332 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3333 """ 3334 if self.accountId is None or not self.accountId: 3335 uLogger.error("Variable `accountId` must be defined for using this method!") 3336 raise Exception("Account ID required") 3337 3338 if orderIDs: 3339 if allOrdersIDs is None or not allOrdersIDs: 3340 rawOrders = self.RequestPendingOrders() 3341 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3342 3343 if allStopOrdersIDs is None or not allStopOrdersIDs: 3344 rawStopOrders = self.RequestStopOrders() 3345 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3346 3347 for orderID in orderIDs: 3348 idInPendingOrders = orderID in allOrdersIDs 3349 idInStopOrders = orderID in allStopOrdersIDs 3350 3351 if not (idInPendingOrders or idInStopOrders): 3352 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3353 continue 3354 3355 else: 3356 if idInPendingOrders: 3357 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3358 3359 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3360 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3361 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3362 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3363 3364 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3365 if self.moreDebug: 3366 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3367 3368 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3369 3370 else: 3371 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3372 3373 elif idInStopOrders: 3374 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3375 3376 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3377 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3378 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3379 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3380 3381 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3382 if self.moreDebug: 3383 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3384 3385 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3386 3387 else: 3388 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3389 3390 else: 3391 continue 3392 3393 def CloseAllOrders(self) -> None: 3394 """ 3395 Gets a list of open pending and stop orders and cancel it all. 3396 """ 3397 rawOrders = self.RequestPendingOrders() 3398 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3399 lenOrders = len(allOrdersIDs) 3400 3401 rawStopOrders = self.RequestStopOrders() 3402 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3403 lenSOrders = len(allStopOrdersIDs) 3404 3405 if lenOrders > 0 or lenSOrders > 0: 3406 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3407 3408 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3409 3410 else: 3411 uLogger.info("Orders not found, nothing to cancel.") 3412 3413 def CloseAll(self, *args) -> None: 3414 """ 3415 Close all available (not blocked) opened trades and orders. 3416 3417 Also, you can select one or more keywords case-insensitive: 3418 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3419 3420 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3421 """ 3422 overview = self.Overview(show=False) # get all open trades info 3423 3424 if len(args) == 0: 3425 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3426 self.CloseAllOrders() # close all pending and stop orders 3427 3428 for iType in TKS_INSTRUMENTS: 3429 if iType != "Currencies": 3430 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3431 3432 else: 3433 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3434 lowerArgs = [x.lower() for x in args] 3435 3436 if "orders" in lowerArgs: 3437 self.CloseAllOrders() # close all pending and stop orders 3438 3439 for iType in TKS_INSTRUMENTS: 3440 if iType.lower() in lowerArgs and iType != "Currencies": 3441 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3442 3443 @staticmethod 3444 def ParseOrderParameters(operation, **inputParameters): 3445 """ 3446 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3447 3448 :param operation: string "Buy" or "Sell". 3449 :param inputParameters: this is dict of strings that looks like this 3450 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3451 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3452 "prices" key: one or more prices to open limit-orders 3453 Counts of values in lots and prices lists must be equals! 3454 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3455 """ 3456 # TODO: update order grid work with api v2 3457 pass 3458 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3459 # 3460 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3461 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3462 # raise Exception("Incorrect value") 3463 # 3464 # if "l" in inputParameters.keys(): 3465 # inputParameters["lots"] = inputParameters.pop("l") 3466 # 3467 # if "p" in inputParameters.keys(): 3468 # inputParameters["prices"] = inputParameters.pop("p") 3469 # 3470 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3471 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3472 # raise Exception("Incorrect value") 3473 # 3474 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3475 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3476 # 3477 # if len(lots) != len(prices): 3478 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3479 # raise Exception("Incorrect value") 3480 # 3481 # uLogger.debug("Extracted parameters for orders:") 3482 # uLogger.debug("lots = {}".format(lots)) 3483 # uLogger.debug("prices = {}".format(prices)) 3484 # 3485 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3486 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3487 # uLogger.debug("Order parameters: {}".format(result)) 3488 # 3489 # return result 3490 3491 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3492 """ 3493 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3494 3495 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3496 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3497 """ 3498 result = False 3499 msg = "Instrument not defined!" 3500 3501 if portfolio is None or not portfolio: 3502 portfolio = self.Overview(show=False) 3503 3504 if self.ticker: 3505 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3506 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3507 3508 for iType in TKS_INSTRUMENTS: 3509 for instrument in portfolio["stat"][iType]: 3510 if instrument["ticker"] == self.ticker: 3511 result = True 3512 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3513 break 3514 3515 elif self.figi: 3516 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3517 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3518 3519 for iType in TKS_INSTRUMENTS: 3520 for instrument in portfolio["stat"][iType]: 3521 if instrument["figi"] == self.figi: 3522 result = True 3523 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3524 break 3525 3526 else: 3527 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3528 3529 uLogger.debug(msg) 3530 3531 return result 3532 3533 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3534 """ 3535 Returns instrument from the user's portfolio if it presents there. 3536 Instrument must be defined by `ticker` (highly priority) or `figi`. 3537 3538 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3539 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3540 """ 3541 result = None 3542 msg = "Instrument not defined!" 3543 3544 if portfolio is None or not portfolio: 3545 portfolio = self.Overview(show=False) 3546 3547 if self.ticker: 3548 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self.ticker)) 3549 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3550 3551 for iType in TKS_INSTRUMENTS: 3552 for instrument in portfolio["stat"][iType]: 3553 if instrument["ticker"] == self.ticker: 3554 result = instrument 3555 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3556 break 3557 3558 elif self.figi: 3559 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3560 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3561 3562 for iType in TKS_INSTRUMENTS: 3563 for instrument in portfolio["stat"][iType]: 3564 if instrument["figi"] == self.figi: 3565 result = instrument 3566 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3567 break 3568 3569 else: 3570 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3571 3572 uLogger.debug(msg) 3573 3574 return result 3575 3576 def RequestLimits(self) -> dict: 3577 """ 3578 Method for obtaining the available funds for withdrawal for current `accountId`. 3579 3580 See also: 3581 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3582 - `OverviewLimits()` method 3583 3584 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3585 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3586 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3587 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3588 """ 3589 if self.accountId is None or not self.accountId: 3590 uLogger.error("Variable `accountId` must be defined for using this method!") 3591 raise Exception("Account ID required") 3592 3593 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3594 3595 self.body = str({"accountId": self.accountId}) 3596 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3597 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3598 3599 if self.moreDebug: 3600 uLogger.debug("Records about available funds for withdrawal successfully received") 3601 3602 return rawLimits 3603 3604 def OverviewLimits(self, show: bool = False) -> dict: 3605 """ 3606 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3607 3608 See also: `RequestLimits()`. 3609 3610 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3611 :return: dict with raw parsed data from server and some calculated statistics about it. 3612 """ 3613 if self.accountId is None or not self.accountId: 3614 uLogger.error("Variable `accountId` must be defined for using this method!") 3615 raise Exception("Account ID required") 3616 3617 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3618 3619 view = { 3620 "rawLimits": rawLimits, 3621 "limits": { # parsed data for every currency: 3622 "money": { # this is an array of portfolio currency positions 3623 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3624 }, 3625 "blocked": { # this is an array of blocked currency 3626 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3627 }, 3628 "blockedGuarantee": { # this is locked money under collateral for futures 3629 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3630 }, 3631 }, 3632 } 3633 3634 # --- Prepare text table with limits in human-readable format: 3635 if show: 3636 info = [ 3637 "# Withdrawal limits\n\n", 3638 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3639 "* **Account ID:** [{}]\n".format(self.accountId), 3640 ] 3641 3642 if view["limits"]["money"]: 3643 info.extend([ 3644 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3645 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3646 ]) 3647 3648 else: 3649 info.append("\nNo withdrawal limits\n") 3650 3651 for curr in view["limits"]["money"].keys(): 3652 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3653 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3654 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3655 3656 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3657 "[{}]".format(curr), 3658 "{:.2f}".format(view["limits"]["money"][curr]), 3659 "{:.2f}".format(availableMoney), 3660 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3661 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3662 ) 3663 3664 if curr == "rub": 3665 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3666 3667 else: 3668 info.append(infoStr) 3669 3670 infoText = "".join(info) 3671 3672 uLogger.info(infoText) 3673 3674 if self.withdrawalLimitsFile: 3675 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3676 fH.write(infoText) 3677 3678 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3679 3680 return view 3681 3682 def RequestAccounts(self) -> dict: 3683 """ 3684 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3685 3686 See also: 3687 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3688 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3689 - `OverviewUserInfo()` method 3690 3691 :return: dict with raw data from server that contains accounts info. Example of dict: 3692 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3693 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3694 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3695 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3696 """ 3697 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3698 3699 self.body = str({}) 3700 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3701 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3702 3703 if self.moreDebug: 3704 uLogger.debug("Records about available accounts successfully received") 3705 3706 return rawAccounts 3707 3708 def RequestUserInfo(self) -> dict: 3709 """ 3710 Method for requesting common user's information. 3711 3712 See also: 3713 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3714 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3715 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3716 - `OverviewUserInfo()` method 3717 3718 :return: dict with raw data from server that contains user's information. Example of dict: 3719 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3720 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3721 """ 3722 uLogger.debug("Requesting common user's information. Wait, please...") 3723 3724 self.body = str({}) 3725 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3726 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3727 3728 if self.moreDebug: 3729 uLogger.debug("Records about current user successfully received") 3730 3731 return rawUserInfo 3732 3733 def RequestMarginStatus(self, accountId: str = None) -> dict: 3734 """ 3735 Method for requesting margin calculation for defined account ID. 3736 3737 See also: 3738 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3739 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3740 - `OverviewUserInfo()` method 3741 3742 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3743 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3744 Example of responses: 3745 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3746 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3747 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3748 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3749 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3750 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3751 """ 3752 if accountId is None or not accountId: 3753 if self.accountId is None or not self.accountId: 3754 uLogger.error("Variable `accountId` must be defined for using this method!") 3755 raise Exception("Account ID required") 3756 3757 else: 3758 accountId = self.accountId # use `self.accountId` (main ID) by default 3759 3760 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3761 3762 self.body = str({"accountId": accountId}) 3763 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3764 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3765 3766 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3767 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3768 rawMargin = {} 3769 3770 else: 3771 if self.moreDebug: 3772 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3773 3774 return rawMargin 3775 3776 def RequestTariffLimits(self) -> dict: 3777 """ 3778 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3779 3780 See also: 3781 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3782 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3783 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3784 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3785 - `OverviewUserInfo()` method 3786 3787 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3788 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3789 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3790 """ 3791 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3792 3793 self.body = str({}) 3794 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3795 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3796 3797 if self.moreDebug: 3798 uLogger.debug("Records with limits of current tariff successfully received") 3799 3800 return rawTariffLimits 3801 3802 def RequestBondCoupons(self, iJSON: dict) -> dict: 3803 """ 3804 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3805 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3806 All dates are in UTC timezone. 3807 3808 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3809 Documentation: 3810 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3811 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3812 3813 See also: `ExtendBondsData()`. 3814 3815 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3816 If raw iJSON is not data of bond then server returns an error [400] with message: 3817 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3818 :return: dictionary with bond payment calendar. Response example 3819 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3820 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3821 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3822 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3823 """ 3824 if iJSON["figi"] is None or not iJSON["figi"]: 3825 uLogger.error("FIGI must be defined for using this method!") 3826 raise Exception("FIGI required") 3827 3828 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3829 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3830 3831 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3832 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3833 self.figi, 3834 startDate, 3835 endDate, 3836 )) 3837 3838 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3839 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3840 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 3841 3842 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3843 uLogger.warning("Instrument type is not bond!") 3844 3845 else: 3846 if self.moreDebug: 3847 uLogger.debug("Records about bond payment calendar successfully received") 3848 3849 return calendar 3850 3851 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3852 """ 3853 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3854 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 3855 coupon yields, current yields and some statistics etc. 3856 3857 WARNING! This is too long operation if a lot of bonds requested from broker server. 3858 3859 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3860 3861 :param instruments: list of strings with tickers or FIGIs. 3862 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3863 for further used by data scientists or stock analytics. 3864 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 3865 In XLSX-file and Pandas DataFrame fields mean: 3866 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3867 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3868 """ 3869 if instruments is None or not instruments: 3870 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3871 raise Exception("Ticker or FIGI required") 3872 3873 if isinstance(instruments, str): 3874 instruments = [instruments] 3875 3876 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3877 3878 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3879 3880 iCount = len(uniqueInstruments) 3881 tooLong = iCount >= 20 3882 if tooLong: 3883 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3884 3885 bonds = None 3886 for i, self.figi in enumerate(uniqueInstruments): 3887 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3888 3889 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3890 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3891 rawBond = self.SearchByFIGI(requestPrice=True) 3892 3893 # Widen raw data with UTC current time (iData["actualDateTime"]): 3894 actualDate = datetime.now(tzutc()) 3895 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3896 3897 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3898 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3899 3900 # Replace some values with human-readable: 3901 iData["nominalCurrency"] = iData["nominal"]["currency"] 3902 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3903 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3904 iData["aciCurrency"] = iData["aciValue"]["currency"] 3905 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3906 iData["issueSize"] = int(iData["issueSize"]) 3907 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3908 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3909 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3910 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3911 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3912 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3913 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3914 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3915 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3916 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3917 3918 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3919 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3920 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3921 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3922 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3923 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3924 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3925 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3926 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3927 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3928 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3929 3930 # Widen raw data with calendar data from `rawCalendar` values: 3931 calendarData = [] 3932 if "events" in iData["rawCalendar"].keys(): 3933 for item in iData["rawCalendar"]["events"]: 3934 calendarData.append({ 3935 "couponDate": item["couponDate"], 3936 "couponNumber": int(item["couponNumber"]), 3937 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3938 "payCurrency": item["payOneBond"]["currency"], 3939 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3940 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3941 "couponStartDate": item["couponStartDate"], 3942 "couponEndDate": item["couponEndDate"], 3943 "couponPeriod": item["couponPeriod"], 3944 }) 3945 3946 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3947 if "maturityDate" not in iData.keys(): 3948 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3949 3950 # Widen raw data with Coupon Rate. 3951 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3952 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3953 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3954 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3955 3956 # Widen raw data with Yield to Maturity (YTM) on current date. 3957 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3958 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3959 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3960 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3961 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3962 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3963 3964 iData["calendar"] = calendarData # adds calendar at the end 3965 3966 # Remove not used data: 3967 iData.pop("uid") 3968 iData.pop("positionUid") 3969 iData.pop("currentPrice") 3970 iData.pop("rawCalendar") 3971 3972 colNames = list(iData.keys()) 3973 if bonds is None: 3974 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3975 3976 else: 3977 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 3978 3979 else: 3980 uLogger.warning("Instrument is not a bond!") 3981 3982 processed = round(100 * (i + 1) / iCount, 1) 3983 if tooLong and processed % 5 == 0: 3984 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 3985 3986 else: 3987 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 3988 3989 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 3990 3991 # Saving bonds from Pandas DataFrame to XLSX sheet: 3992 if xlsx and self.bondsXLSXFile: 3993 with pd.ExcelWriter( 3994 path=self.bondsXLSXFile, 3995 date_format=TKS_DATE_FORMAT, 3996 datetime_format=TKS_DATE_TIME_FORMAT, 3997 mode="w", 3998 ) as writer: 3999 bonds.to_excel( 4000 writer, 4001 sheet_name="Extended bonds data", 4002 index=True, 4003 encoding="UTF-8", 4004 freeze_panes=(1, 1), 4005 ) # saving as XLSX-file with freeze first row and column as headers 4006 4007 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4008 4009 return bonds 4010 4011 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4012 """ 4013 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4014 4015 WARNING! This is too long operation if a lot of bonds requested from broker server. 4016 4017 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4018 4019 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4020 extended information about bonds: main info, current prices, bond payment calendar, 4021 coupon yields, current yields and some statistics etc. 4022 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4023 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4024 for further used by data scientists or stock analytics. 4025 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4026 """ 4027 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4028 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4029 4030 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4031 4032 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4033 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4034 calendar = None 4035 for bond in extBonds.iterrows(): 4036 for item in bond[1]["calendar"]: 4037 cData = { 4038 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4039 "couponDate": item["couponDate"], 4040 "figi": bond[1]["figi"], 4041 "ticker": bond[1]["ticker"], 4042 "name": bond[1]["name"], 4043 "couponNumber": item["couponNumber"], 4044 "payOneBond": item["payOneBond"], 4045 "payCurrency": item["payCurrency"], 4046 "couponType": item["couponType"], 4047 "couponPeriod": item["couponPeriod"], 4048 "fixDate": item["fixDate"], 4049 "couponStartDate": item["couponStartDate"], 4050 "couponEndDate": item["couponEndDate"], 4051 } 4052 4053 if calendar is None: 4054 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4055 4056 else: 4057 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4058 4059 if calendar is not None: 4060 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4061 4062 # Saving calendar from Pandas DataFrame to XLSX sheet: 4063 if xlsx: 4064 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4065 4066 with pd.ExcelWriter( 4067 path=xlsxCalendarFile, 4068 date_format=TKS_DATE_FORMAT, 4069 datetime_format=TKS_DATE_TIME_FORMAT, 4070 mode="w", 4071 ) as writer: 4072 humanReadable = calendar.copy(deep=True) 4073 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4074 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4075 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4076 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4077 humanReadable.columns = colNames # human-readable column names 4078 4079 humanReadable.to_excel( 4080 writer, 4081 sheet_name="Bond payments calendar", 4082 index=False, 4083 encoding="UTF-8", 4084 freeze_panes=(1, 2), 4085 ) # saving as XLSX-file with freeze first row and column as headers 4086 4087 del humanReadable # release df in memory 4088 4089 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4090 4091 return calendar 4092 4093 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4094 """ 4095 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4096 Also, creates Markdown file with calendar data, `calendar.md` by default. 4097 4098 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4099 4100 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4101 extended information about bonds: main info, current prices, bond payment calendar, 4102 coupon yields, current yields and some statistics etc. 4103 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4104 :param show: if `True` then also printing bonds payment calendar to the console, 4105 otherwise save to file `calendarFile` only. `False` by default. 4106 :return: multilines text in Markdown format with bonds payment calendar as a table. 4107 """ 4108 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4109 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4110 4111 infoText = "# Bond payments calendar\n\n" 4112 4113 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4114 4115 if not (calendar is None or calendar.empty): 4116 splitLine = "| | | | | | | | | |\n" 4117 4118 info = [ 4119 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4120 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4121 ] 4122 4123 newMonth = False 4124 notOneBond = calendar["figi"].nunique() > 1 4125 for i, bond in enumerate(calendar.iterrows()): 4126 if newMonth and notOneBond: 4127 info.append(splitLine) 4128 4129 info.append( 4130 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4131 " √" if bond[1]["paid"] else " —", 4132 bond[1]["couponDate"].split("T")[0], 4133 bond[1]["figi"], 4134 bond[1]["ticker"], 4135 bond[1]["couponNumber"], 4136 "{} {}".format( 4137 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4138 bond[1]["payCurrency"], 4139 ), 4140 bond[1]["couponType"], 4141 bond[1]["couponPeriod"], 4142 bond[1]["fixDate"].split("T")[0], 4143 ) 4144 ) 4145 4146 if i < len(calendar.values) - 1: 4147 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4148 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4149 newMonth = False if curDate.month == nextDate.month else True 4150 4151 else: 4152 newMonth = False 4153 4154 infoText += "".join(info) 4155 4156 if show: 4157 uLogger.info("{}".format(infoText)) 4158 4159 if self.calendarFile is not None: 4160 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4161 fH.write(infoText) 4162 4163 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4164 4165 else: 4166 infoText += "No data\n" 4167 4168 return infoText 4169 4170 def OverviewAccounts(self, show: bool = False) -> dict: 4171 """ 4172 Method for parsing and show simple table with all available user accounts. 4173 4174 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4175 4176 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4177 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4178 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4179 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4180 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4181 "closed": "—", "access": "Full access" }, ...}}` 4182 """ 4183 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4184 4185 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4186 accounts = { 4187 item["id"]: { 4188 "type": TKS_ACCOUNT_TYPES[item["type"]], 4189 "name": item["name"], 4190 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4191 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4192 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4193 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4194 } for item in rawAccounts["accounts"] 4195 } 4196 4197 # Raw and parsed data with some fields replaced in "stat" section: 4198 view = { 4199 "rawAccounts": rawAccounts, 4200 "stat": accounts, 4201 } 4202 4203 # --- Prepare simple text table with only accounts data in human-readable format: 4204 if show: 4205 info = [ 4206 "# User accounts\n\n", 4207 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4208 "| Account ID | Type | Status | Name |\n", 4209 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4210 ] 4211 4212 for account in view["stat"].keys(): 4213 info.extend([ 4214 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4215 account, 4216 view["stat"][account]["type"], 4217 view["stat"][account]["status"], 4218 view["stat"][account]["name"], 4219 ) 4220 ]) 4221 4222 infoText = "".join(info) 4223 4224 uLogger.info(infoText) 4225 4226 if self.userAccountsFile: 4227 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4228 fH.write(infoText) 4229 4230 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4231 4232 return view 4233 4234 def OverviewUserInfo(self, show: bool = False) -> dict: 4235 """ 4236 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4237 4238 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4239 4240 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4241 :return: dict with raw parsed data from server and some calculated statistics about it. 4242 """ 4243 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4244 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4245 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4246 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4247 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4248 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4249 4250 # This is dict with parsed common user data: 4251 userInfo = { 4252 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4253 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4254 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4255 "tariff": rawUserInfo["tariff"], 4256 } 4257 4258 # This is an array of dict with parsed margin statuses for every account IDs: 4259 margins = {} 4260 for accountId in accounts.keys(): 4261 if rawMargins[accountId]: 4262 margins[accountId] = { 4263 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4264 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4265 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4266 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4267 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4268 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4269 } 4270 4271 else: 4272 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4273 4274 unary = {} # unary-connection limits 4275 for item in rawTariffLimits["unaryLimits"]: 4276 if item["limitPerMinute"] in unary.keys(): 4277 unary[item["limitPerMinute"]].extend(item["methods"]) 4278 4279 else: 4280 unary[item["limitPerMinute"]] = item["methods"] 4281 4282 stream = {} # stream-connection limits 4283 for item in rawTariffLimits["streamLimits"]: 4284 if item["limit"] in stream.keys(): 4285 stream[item["limit"]].extend(item["streams"]) 4286 4287 else: 4288 stream[item["limit"]] = item["streams"] 4289 4290 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4291 limits = { 4292 "unary": unary, 4293 "stream": stream, 4294 } 4295 4296 # Raw and parsed data as an output result: 4297 view = { 4298 "rawUserInfo": rawUserInfo, 4299 "rawAccounts": rawAccounts, 4300 "rawMargins": rawMargins, 4301 "rawTariffLimits": rawTariffLimits, 4302 "stat": { 4303 "userInfo": userInfo, 4304 "accounts": accounts, 4305 "margins": margins, 4306 "limits": limits, 4307 }, 4308 } 4309 4310 # --- Prepare text table with user information in human-readable format: 4311 if show: 4312 info = [ 4313 "# Full user information\n\n", 4314 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4315 "## Common information\n\n", 4316 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4317 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4318 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4319 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4320 "\n## User accounts\n\n", 4321 ] 4322 4323 for account in view["stat"]["accounts"].keys(): 4324 info.extend([ 4325 "### ID: [{}]\n\n".format(account), 4326 "| Parameters | Values |\n", 4327 "|----------------------|--------------------------------------------------------------|\n", 4328 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4329 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4330 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4331 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4332 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4333 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4334 ]) 4335 4336 if margins[account]: 4337 info.extend([ 4338 "| Margin status: | Enabled |\n", 4339 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4340 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4341 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4342 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4343 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4344 ]) 4345 4346 else: 4347 info.append("| Margin status: | Disabled |\n\n") 4348 4349 info.extend([ 4350 "\n## Current user tariff limits\n", 4351 "\nSee also:\n", 4352 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4353 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4354 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4355 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4356 "\n### Unary limits\n", 4357 ]) 4358 4359 if unary: 4360 for key, values in sorted(unary.items()): 4361 info.append("\n* Max requests per minute: {}\n".format(key)) 4362 4363 for value in values: 4364 info.append(" - {}\n".format(value)) 4365 4366 else: 4367 info.append("\nNot available\n") 4368 4369 info.append("\n### Stream limits\n") 4370 4371 if stream: 4372 for key, values in sorted(stream.items()): 4373 info.append("\n* Max stream connections: {}\n".format(key)) 4374 4375 for value in values: 4376 info.append(" - {}\n".format(value)) 4377 4378 else: 4379 info.append("\nNot available\n") 4380 4381 infoText = "".join(info) 4382 4383 uLogger.info(infoText) 4384 4385 if self.userInfoFile: 4386 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4387 fH.write(infoText) 4388 4389 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4390 4391 return view 4392 4393 4394class Args: 4395 """ 4396 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4397 """ 4398 def __init__(self, **kwargs): 4399 self.__dict__.update(kwargs) 4400 4401 def __getattr__(self, item): 4402 return None 4403 4404 4405def ParseArgs(): 4406 """This function get and parse command line keys.""" 4407 parser = ArgumentParser() # command-line string parser 4408 4409 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4410 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4411 4412 # --- options: 4413 4414 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4415 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4416 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4417 4418 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4419 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4420 4421 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4422 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4423 4424 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4425 4426 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4427 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4428 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4429 4430 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4431 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4432 4433 # --- commands: 4434 4435 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4436 4437 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4438 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4439 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4440 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4441 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4442 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4443 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4444 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4445 4446 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4447 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4448 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4449 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4450 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4451 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4452 4453 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4454 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4455 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4456 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4457 4458 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4459 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4460 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4461 4462 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4463 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4464 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4465 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4466 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4467 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4468 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4469 4470 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4471 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4472 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4473 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4474 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.") 4475 4476 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4477 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4478 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4479 4480 cmdArgs = parser.parse_args() 4481 return cmdArgs 4482 4483 4484def Main(**kwargs): 4485 """ 4486 Main function for work with TKSBrokerAPI in the console. 4487 4488 See examples: 4489 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4490 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4491 """ 4492 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4493 4494 if args.debug_level: 4495 uLogger.level = 10 # always debug level by default 4496 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4497 4498 exitCode = 0 4499 start = datetime.now(tzutc()) 4500 uLogger.debug("=-" * 50) 4501 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4502 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4503 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4504 )) 4505 4506 # trying to calculate full current version: 4507 buildVersion = __version__ 4508 try: 4509 v = version("tksbrokerapi") 4510 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4511 4512 except Exception: 4513 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4514 4515 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4516 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4517 4518 try: 4519 if args.version: 4520 print("TKSBrokerAPI {}".format(buildVersion)) 4521 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4522 4523 else: 4524 # Init class for trading with Tinkoff Broker: 4525 trader = TinkoffBrokerServer( 4526 token=args.token, 4527 accountId=args.account_id, 4528 useCache=not args.no_cache, 4529 ) 4530 4531 # --- set some options: 4532 4533 if args.more: 4534 trader.moreDebug = True 4535 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4536 4537 if args.ticker: 4538 ticker = args.ticker.upper() # Tickers may be upper case only 4539 4540 if ticker in trader.aliasesKeys: 4541 trader.ticker = trader.aliases[ticker] # Replace some tickers with its aliases 4542 4543 else: 4544 trader.ticker = ticker 4545 4546 if args.figi: 4547 trader.figi = args.figi.upper() # FIGIs may be upper case only 4548 4549 if args.depth is not None: 4550 trader.depth = args.depth 4551 4552 # --- do one command: 4553 4554 if args.list: 4555 if args.output is not None: 4556 trader.instrumentsFile = args.output 4557 4558 trader.ShowInstrumentsInfo(show=True) 4559 4560 elif args.list_xlsx: 4561 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4562 4563 elif args.bonds_xlsx is not None: 4564 if args.output is not None: 4565 trader.bondsXLSXFile = args.output 4566 4567 if len(args.bonds_xlsx) == 0: 4568 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4569 4570 else: 4571 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4572 4573 elif args.search: 4574 if args.output is not None: 4575 trader.searchResultsFile = args.output 4576 4577 trader.SearchInstruments(pattern=args.search[0], show=True) 4578 4579 elif args.info: 4580 if not (args.ticker or args.figi): 4581 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4582 raise Exception("Ticker or FIGI required") 4583 4584 if args.output is not None: 4585 trader.infoFile = args.output 4586 4587 if args.ticker: 4588 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4589 4590 else: 4591 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4592 4593 elif args.calendar is not None: 4594 if args.output is not None: 4595 trader.calendarFile = args.output 4596 4597 if len(args.calendar) == 0: 4598 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4599 4600 else: 4601 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4602 4603 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4604 4605 elif args.price: 4606 if not (args.ticker or args.figi): 4607 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4608 raise Exception("Ticker or FIGI required") 4609 4610 trader.GetCurrentPrices(show=True) 4611 4612 elif args.prices is not None: 4613 if args.output is not None: 4614 trader.pricesFile = args.output 4615 4616 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4617 4618 elif args.overview: 4619 if args.output is not None: 4620 trader.overviewFile = args.output 4621 4622 trader.Overview(show=True, details="full") 4623 4624 elif args.overview_digest: 4625 if args.output is not None: 4626 trader.overviewDigestFile = args.output 4627 4628 trader.Overview(show=True, details="digest") 4629 4630 elif args.overview_positions: 4631 if args.output is not None: 4632 trader.overviewPositionsFile = args.output 4633 4634 trader.Overview(show=True, details="positions") 4635 4636 elif args.overview_orders: 4637 if args.output is not None: 4638 trader.overviewOrdersFile = args.output 4639 4640 trader.Overview(show=True, details="orders") 4641 4642 elif args.overview_analytics: 4643 if args.output is not None: 4644 trader.overviewAnalyticsFile = args.output 4645 4646 trader.Overview(show=True, details="analytics") 4647 4648 elif args.overview_calendar: 4649 if args.output is not None: 4650 trader.overviewAnalyticsFile = args.output 4651 4652 trader.Overview(show=True, details="calendar") 4653 4654 elif args.deals is not None: 4655 if args.output is not None: 4656 trader.reportFile = args.output 4657 4658 if 0 <= len(args.deals) < 3: 4659 trader.Deals( 4660 start=args.deals[0] if len(args.deals) >= 1 else None, 4661 end=args.deals[1] if len(args.deals) == 2 else None, 4662 show=True, # Always show deals report in console 4663 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4664 ) 4665 4666 else: 4667 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4668 raise Exception("Incorrect value") 4669 4670 elif args.history is not None: 4671 if args.output is not None: 4672 trader.historyFile = args.output 4673 4674 if 0 <= len(args.history) < 3: 4675 dataReceived = trader.History( 4676 start=args.history[0] if len(args.history) >= 1 else None, 4677 end=args.history[1] if len(args.history) == 2 else None, 4678 interval="hour" if args.interval is None or not args.interval else args.interval, 4679 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 4680 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 4681 show=True, # shows all downloaded candles in console 4682 ) 4683 4684 if args.render_chart is not None and dataReceived is not None: 4685 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4686 4687 trader.ShowHistoryChart( 4688 candles=dataReceived, 4689 interact=iChart, 4690 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4691 ) 4692 4693 else: 4694 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4695 raise Exception("Incorrect value") 4696 4697 elif args.load_history is not None: 4698 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 4699 4700 if args.render_chart is not None and histData is not None: 4701 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4702 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 4703 4704 trader.ShowHistoryChart( 4705 candles=histData, 4706 interact=iChart, 4707 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4708 ) 4709 4710 elif args.trade is not None: 4711 if 1 <= len(args.trade) <= 5: 4712 trader.Trade( 4713 operation=args.trade[0], 4714 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 4715 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 4716 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 4717 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 4718 ) 4719 4720 else: 4721 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4722 4723 elif args.buy is not None: 4724 if 0 <= len(args.buy) <= 4: 4725 trader.Buy( 4726 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 4727 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 4728 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 4729 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 4730 ) 4731 4732 else: 4733 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4734 4735 elif args.sell is not None: 4736 if 0 <= len(args.sell) <= 4: 4737 trader.Sell( 4738 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 4739 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 4740 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 4741 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 4742 ) 4743 4744 else: 4745 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4746 4747 elif args.order: 4748 if 4 <= len(args.order) <= 7: 4749 trader.Order( 4750 operation=args.order[0], 4751 orderType=args.order[1], 4752 lots=int(args.order[2]), 4753 targetPrice=float(args.order[3]), 4754 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 4755 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 4756 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 4757 ) 4758 4759 else: 4760 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 4761 4762 elif args.buy_limit: 4763 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 4764 4765 elif args.sell_limit: 4766 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 4767 4768 elif args.buy_stop: 4769 if 2 <= len(args.buy_stop) <= 7: 4770 trader.BuyStop( 4771 lots=int(args.buy_stop[0]), 4772 targetPrice=float(args.buy_stop[1]), 4773 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 4774 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 4775 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 4776 ) 4777 4778 else: 4779 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4780 4781 elif args.sell_stop: 4782 if 2 <= len(args.sell_stop) <= 7: 4783 trader.SellStop( 4784 lots=int(args.sell_stop[0]), 4785 targetPrice=float(args.sell_stop[1]), 4786 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 4787 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 4788 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 4789 ) 4790 4791 else: 4792 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 4793 4794 # elif args.buy_order_grid is not None: 4795 # # update order grid work with api v2 4796 # if len(args.buy_order_grid) == 2: 4797 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 4798 # 4799 # for order in orderParams: 4800 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 4801 # 4802 # else: 4803 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4804 # 4805 # elif args.sell_order_grid is not None: 4806 # # update order grid work with api v2 4807 # if len(args.sell_order_grid) >= 2: 4808 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 4809 # 4810 # for order in orderParams: 4811 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 4812 # 4813 # else: 4814 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4815 4816 elif args.close_order is not None: 4817 trader.CloseOrders(args.close_order) # close only one order 4818 4819 elif args.close_orders is not None: 4820 trader.CloseOrders(args.close_orders) # close list of orders 4821 4822 elif args.close_trade: 4823 if not (args.ticker or args.figi): 4824 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4825 raise Exception("Ticker or FIGI required") 4826 4827 if args.ticker: 4828 trader.CloseTrades([args.ticker]) # close only one trade by ticker (priority) 4829 4830 else: 4831 trader.CloseTrades([args.figi]) # close only one trade by FIGI 4832 4833 elif args.close_trades is not None: 4834 trader.CloseTrades(args.close_trades) # close trades for list of tickers 4835 4836 elif args.close_all is not None: 4837 trader.CloseAll(*args.close_all) 4838 4839 elif args.limits: 4840 if args.output is not None: 4841 trader.withdrawalLimitsFile = args.output 4842 4843 trader.OverviewLimits(show=True) 4844 4845 elif args.user_info: 4846 if args.output is not None: 4847 trader.userInfoFile = args.output 4848 4849 trader.OverviewUserInfo(show=True) 4850 4851 elif args.account: 4852 if args.output is not None: 4853 trader.userAccountsFile = args.output 4854 4855 trader.OverviewAccounts(show=True) 4856 4857 else: 4858 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 4859 raise Exception("There is no command to execute") 4860 4861 except Exception: 4862 trace = tb.format_exc() 4863 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 4864 if e in trace: 4865 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 4866 break 4867 4868 uLogger.debug(trace) 4869 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 4870 exitCode = 255 # an error occurred, must be open a ticket for this issue 4871 4872 finally: 4873 finish = datetime.now(tzutc()) 4874 4875 if exitCode == 0: 4876 if args.more: 4877 uLogger.debug("All operations were finished success (summary code is 0).") 4878 4879 else: 4880 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 4881 os.path.abspath(uLog.defaultLogFile), exitCode, 4882 )) 4883 4884 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 4885 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 4886 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4887 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4888 )) 4889 uLogger.debug("=-" * 50) 4890 4891 if not kwargs: 4892 sys.exit(exitCode) 4893 4894 else: 4895 return exitCode 4896 4897 4898if __name__ == "__main__": 4899 Main()
77def GetDatesAsString(start: str = None, end: str = None) -> tuple: 78 """ 79 Create tuple of date and time strings with timezone parsed from user-friendly date. 80 81 User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020). 82 83 Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") 84 An error exception will occur if input date has incorrect format. 85 86 If `start=None`, `end=None` then return dates from yesterday to the end of the day. 87 If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day. 88 If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`. 89 Start day may be negative integer numbers: `-1`, `-2`, `-3` — how many days ago. 90 91 Also, you can use keywords for start if `end=None`: 92 `today` (from 00:00:00 to the end of current day), 93 `yesterday` (-1 day from 00:00:00 to 23:59:59), 94 `week` (-7 day from 00:00:00 to the end of current day), 95 `month` (-30 day from 00:00:00 to the end of current day), 96 `year` (-365 day from 00:00:00 to the end of current day), 97 98 :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI. 99 See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`. 100 Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day. 101 """ 102 uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end)) 103 s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0) # start of the current day 104 e = s.replace(hour=23, minute=59, second=59, microsecond=0) # end of the current day 105 106 # time between start and the end of the current day: 107 if start is None or start.lower() == "today": 108 pass 109 110 # from start of the last day to the end of the last day: 111 elif start.lower() == "yesterday": 112 s -= timedelta(days=1) 113 e -= timedelta(days=1) 114 115 # week (-7 day from 00:00:00 to the end of the current day): 116 elif start.lower() == "week": 117 s -= timedelta(days=6) # +1 current day already taken into account 118 119 # month (-30 day from 00:00:00 to the end of current day): 120 elif start.lower() == "month": 121 s -= timedelta(days=29) # +1 current day already taken into account 122 123 # year (-365 day from 00:00:00 to the end of current day): 124 elif start.lower() == "year": 125 s -= timedelta(days=364) # +1 current day already taken into account 126 127 # -N days ago to the end of current day: 128 elif start.startswith('-') and start[1:].isdigit(): 129 s -= timedelta(days=abs(int(start)) - 1) # +1 current day already taken into account 130 131 # dates between start day at 00:00:00 and the end of the last day at 23:59:59: 132 else: 133 s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc()) 134 e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e 135 136 # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API: 137 s = s.strftime(TKS_DATE_TIME_FORMAT) 138 e = e.strftime(TKS_DATE_TIME_FORMAT) 139 140 uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e)) 141 142 return s, e
Create tuple of date and time strings with timezone parsed from user-friendly date.
User dates format must be like: %Y-%m-%d, e.g. 2020-02-03 (3 Feb, 2020).
Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") An error exception will occur if input date has incorrect format.
If start=None, end=None then return dates from yesterday to the end of the day.
If start=some_date_1, end=None then return dates from some_date_1 to the end of the day.
If start=some_date_1, end=some_date_2 then return dates from start of some_date_1 to end of some_date_2.
Start day may be negative integer numbers: -1, -2, -3 — how many days ago.
Also, you can use keywords for start if end=None:
today (from 00:00:00 to the end of current day),
yesterday (-1 day from 00:00:00 to 23:59:59),
week (-7 day from 00:00:00 to the end of current day),
month (-30 day from 00:00:00 to the end of current day),
year (-365 day from 00:00:00 to the end of current day),
Returns
tuple with 2 strings
(start, end)dates in UTC ISO time format%Y-%m-%dT%H:%M:%SZfor OpenAPI. See date and time format here:TKSEnums.TKS_DATE_TIME_FORMAT. Example:("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z"). Second string is the end of the last day.
145class TinkoffBrokerServer: 146 """ 147 This class implements methods to work with Tinkoff broker server. 148 149 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 150 151 About `token`: https://tinkoff.github.io/investAPI/token/ 152 """ 153 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 154 """ 155 Main class init. 156 157 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 158 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 159 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 160 :param useCache: use default cache file with raw data to use instead of `iList`. 161 True by default. Cache is auto-update if new day has come. 162 If you don't want to use cache and always updates raw data then set `useCache=False`. 163 :param defaultCache: path to default cache file. `dump.json` by default. 164 """ 165 if token is None or not token: 166 try: 167 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 168 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 169 170 except KeyError: 171 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 172 raise Exception("Token required") 173 174 else: 175 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 176 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 177 178 if accountId is None or not accountId: 179 try: 180 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 181 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 182 183 except KeyError: 184 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 185 186 else: 187 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 188 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 189 190 self.version = __version__ # duplicate here used TKSBrokerAPI main version 191 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 192 193 Latest version: https://pypi.org/project/tksbrokerapi/ 194 """ 195 196 self.aliases = TKS_TICKER_ALIASES 197 """Some aliases instead official tickers. 198 199 See also: `TKSEnums.TKS_TICKER_ALIASES` 200 """ 201 202 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 203 204 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 205 206 self.ticker = "" 207 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 208 209 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 210 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 211 212 See also: `SearchByTicker()`, `SearchInstruments()`. 213 """ 214 215 self.figi = "" 216 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 217 218 See also: `SearchByFIGI()`, `SearchInstruments()`. 219 """ 220 221 self.depth = 1 222 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 223 224 See also: `GetCurrentPrices()`. 225 """ 226 227 self.server = r"https://invest-public-api.tinkoff.ru/rest" 228 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 229 230 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 231 """ 232 233 uLogger.debug("Broker API server: {}".format(self.server)) 234 235 self.timeout = 15 236 """Server operations timeout in seconds. Default: `15`. 237 238 See also: `SendAPIRequest()`. 239 """ 240 241 self.headers = { 242 "Content-Type": "application/json", 243 "accept": "application/json", 244 "Authorization": "Bearer {}".format(self.token), 245 "x-app-name": "Tim55667757.TKSBrokerAPI", 246 } 247 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 248 249 See also: `SendAPIRequest()`. 250 """ 251 252 self.body = None 253 """Request body which send to broker server. Default: `None`. 254 255 See also: `SendAPIRequest()`. 256 """ 257 258 self.moreDebug = False 259 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 260 261 self.historyFile = None 262 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 263 264 See also: `History()`. 265 """ 266 267 self.htmlHistoryFile = "index.html" 268 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 269 270 See also: `ShowHistoryChart()`. 271 """ 272 273 self.instrumentsFile = "instruments.md" 274 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 275 276 See also: `ShowInstrumentsInfo()`. 277 """ 278 279 self.searchResultsFile = "search-results.md" 280 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 281 282 See also: `SearchInstruments()`. 283 """ 284 285 self.pricesFile = "prices.md" 286 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 287 288 See also: `GetListOfPrices()`. 289 """ 290 291 self.infoFile = "info.md" 292 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 293 294 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 295 """ 296 297 self.bondsXLSXFile = "ext-bonds.xlsx" 298 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 299 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 300 301 See also: `ExtendBondsData()`. 302 """ 303 304 self.calendarFile = "calendar.md" 305 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 306 307 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 308 309 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 310 """ 311 312 self.overviewFile = "overview.md" 313 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 314 315 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 316 """ 317 318 self.overviewDigestFile = "overview-digest.md" 319 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 320 321 See also: `Overview()` with parameter `details="digest"`. 322 """ 323 324 self.overviewPositionsFile = "overview-positions.md" 325 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 326 327 See also: `Overview()` with parameter `details="positions"`. 328 """ 329 330 self.overviewOrdersFile = "overview-orders.md" 331 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 332 333 See also: `Overview()` with parameter `details="orders"`. 334 """ 335 336 self.overviewAnalyticsFile = "overview-analytics.md" 337 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 338 339 See also: `Overview()` with parameter `details="analytics"`. 340 """ 341 342 self.overviewBondsCalendarFile = "overview-calendar.md" 343 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 344 345 See also: `Overview()` with parameter `details="calendar"`. 346 """ 347 348 self.reportFile = "deals.md" 349 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 350 351 See also: `Deals()`. 352 """ 353 354 self.withdrawalLimitsFile = "limits.md" 355 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 356 357 See also: `OverviewLimits()` and `RequestLimits()`. 358 """ 359 360 self.userInfoFile = "user-info.md" 361 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 362 363 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 364 """ 365 366 self.userAccountsFile = "accounts.md" 367 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 368 369 See also: `OverviewAccounts()`, `RequestAccounts()`. 370 """ 371 372 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 373 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 374 375 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 376 377 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 378 """ 379 380 self.iList = None # init iList for raw instruments data 381 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 382 383 See also: `Listing()`, `DumpInstruments()`. 384 """ 385 386 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 387 if useCache: 388 if os.path.exists(self.iListDumpFile): 389 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 390 curTime = datetime.now(tzutc()) 391 392 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 393 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 394 395 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 396 397 else: 398 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 399 400 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 401 os.path.abspath(self.iListDumpFile), 402 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 403 )) 404 405 else: 406 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 407 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 408 409 else: 410 self.iList = self.Listing() # request new raw instruments data from broker server 411 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 412 413 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 414 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 415 416 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 417 """ 418 419 def _ParseJSON(self, rawData="{}") -> dict: 420 """ 421 Parse JSON from response string. 422 423 :param rawData: this is a string with JSON-formatted text. 424 :return: JSON (dictionary), parsed from server response string. 425 """ 426 responseJSON = json.loads(rawData) if rawData else {} 427 428 if self.moreDebug: 429 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 430 431 return responseJSON 432 433 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 434 """ 435 Send GET or POST request to broker server and receive JSON object. 436 437 self.header: must be defining with dictionary of headers. 438 self.body: if define then used as request body. None by default. 439 self.timeout: global request timeout, 15 seconds by default. 440 :param url: url with REST request. 441 :param reqType: send "GET" or "POST" request. "GET" by default. 442 :param retry: how many times retry after first request if an 5xx server errors occurred. 443 :param pause: sleep time in seconds between retries. 444 :return: response JSON (dictionary) from broker. 445 """ 446 if reqType not in ("GET", "POST"): 447 uLogger.error("You can define request type: 'GET' or 'POST'!") 448 raise Exception("Incorrect value") 449 450 if self.moreDebug: 451 uLogger.debug("Request parameters:") 452 uLogger.debug(" - REST API URL: {}".format(url)) 453 uLogger.debug(" - request type: {}".format(reqType)) 454 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 455 uLogger.debug(" - body:\n{}".format(self.body)) 456 457 # fast hack to avoid all operations with some tickers/FIGI 458 responseJSON = {} 459 oK = True 460 for item in self.exclude: 461 if item in url: 462 if self.moreDebug: 463 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 464 465 oK = False 466 break 467 468 if oK: 469 counter = 0 470 response = None 471 errMsg = "" 472 473 while not response and counter <= retry: 474 if reqType == "GET": 475 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 476 477 if reqType == "POST": 478 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 479 480 if self.moreDebug: 481 uLogger.debug("Response:") 482 uLogger.debug(" - status code: {}".format(response.status_code)) 483 uLogger.debug(" - reason: {}".format(response.reason)) 484 uLogger.debug(" - body length: {}".format(len(response.text))) 485 uLogger.debug(" - headers:\n{}".format(response.headers)) 486 487 # Server returns some headers: 488 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 489 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 490 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 491 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 492 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 493 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 494 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 495 sleep(rateLimitWait) 496 497 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 498 if 400 <= response.status_code < 500: 499 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 500 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 501 counter = retry + 1 502 503 if 500 <= response.status_code < 600: 504 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 505 uLogger.debug(" - not oK, {}".format(errMsg)) 506 counter += 1 507 508 if counter <= retry: 509 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 510 sleep(pause) 511 512 responseJSON = self._ParseJSON(rawData=response.text) 513 514 if errMsg: 515 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 516 uLogger.error(" - not oK, {}".format(errMsg)) 517 518 return responseJSON 519 520 def _IUpdater(self, iType: str) -> tuple: 521 """ 522 Request instrument by type from server. See available API methods for instruments: 523 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 524 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 525 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 526 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 527 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 528 529 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 530 :return: tuple with iType name and list of available instruments of current type for defined user token. 531 """ 532 result = [] 533 534 if iType in TKS_INSTRUMENTS: 535 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 536 537 # all instruments have the same body in API v2 requests: 538 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 539 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 540 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 541 542 return iType, result 543 544 def _IWrapper(self, kwargs): 545 """ 546 Wrapper runs instrument's update method `_IUpdater()`. 547 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 548 """ 549 return self._IUpdater(**kwargs) 550 551 def Listing(self) -> dict: 552 """ 553 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 554 555 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 556 """ 557 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 558 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 559 560 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 561 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 562 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 563 564 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 565 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 566 poolUpdater.close() 567 568 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 569 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 570 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 571 572 # calculate minimum price increment (step) for all instruments and set up instrument's type: 573 for iType in iList.keys(): 574 for ticker in iList[iType]: 575 iList[iType][ticker]["type"] = iType 576 577 if "minPriceIncrement" in iList[iType][ticker].keys(): 578 iList[iType][ticker]["step"] = NanoToFloat( 579 iList[iType][ticker]["minPriceIncrement"]["units"], 580 iList[iType][ticker]["minPriceIncrement"]["nano"], 581 ) 582 583 else: 584 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 585 586 return iList 587 588 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 589 """ 590 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 591 592 See also: `DumpInstruments()`, `Listing()`. 593 594 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 595 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 596 """ 597 if self.iListDumpFile is None or not self.iListDumpFile: 598 uLogger.error("Output name of dump file must be defined!") 599 raise Exception("Filename required") 600 601 if not self.iList or forceUpdate: 602 self.iList = self.Listing() 603 604 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 605 606 # Save as XLSX with separated sheets for every type of instruments: 607 with pd.ExcelWriter( 608 path=xlsxDumpFile, 609 date_format=TKS_DATE_FORMAT, 610 datetime_format=TKS_DATE_TIME_FORMAT, 611 mode="w", 612 ) as writer: 613 for iType in TKS_INSTRUMENTS: 614 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 615 df = df[sorted(df)] # sorted by column names 616 df = df.applymap( 617 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 618 na_action="ignore", 619 ) # converting numbers from nano-type to float in every cell 620 df.to_excel( 621 writer, 622 sheet_name=iType, 623 encoding="UTF-8", 624 freeze_panes=(1, 1), 625 ) # saving as XLSX-file with freeze first row and column as headers 626 627 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 628 629 def DumpInstruments(self, forceUpdate: bool = True) -> str: 630 """ 631 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 632 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 633 634 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 635 636 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 637 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 638 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 639 """ 640 if self.iListDumpFile is None or not self.iListDumpFile: 641 uLogger.error("Output name of dump file must be defined!") 642 raise Exception("Filename required") 643 644 if not self.iList or forceUpdate: 645 self.iList = self.Listing() 646 647 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 648 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 649 fH.write(jsonDump) 650 651 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 652 653 return jsonDump 654 655 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 656 """ 657 Show information about one instrument defined by json data and prints it in Markdown format. 658 659 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 660 661 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 662 :param show: if `True` then also printing information about instrument and its current price. 663 :return: multilines text in Markdown format with information about one instrument. 664 """ 665 splitLine = "| | |\n" 666 infoText = "" 667 668 if iJSON is not None and iJSON and isinstance(iJSON, dict): 669 info = [ 670 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 671 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 672 "| Parameters | Values |\n", 673 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 674 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 675 "| Full name: | {:<54} |\n".format(iJSON["name"]), 676 ] 677 678 if "sector" in iJSON.keys() and iJSON["sector"]: 679 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 680 681 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 682 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 683 684 info.extend([ 685 splitLine, 686 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 687 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 688 ]) 689 690 if "isin" in iJSON.keys() and iJSON["isin"]: 691 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 692 693 if "classCode" in iJSON.keys(): 694 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 695 696 info.extend([ 697 splitLine, 698 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 699 splitLine, 700 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 701 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 702 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 703 ]) 704 705 if iJSON["figi"]: 706 self.figi = iJSON["figi"] 707 iJSON = iJSON | self.RequestTradingStatus() 708 709 info.extend([ 710 splitLine, 711 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 712 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 713 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 714 ]) 715 716 info.append(splitLine) 717 718 if "type" in iJSON.keys() and iJSON["type"]: 719 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 720 721 if "shareType" in iJSON.keys() and iJSON["shareType"]: 722 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 723 724 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 725 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 726 727 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 728 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 729 730 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 731 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 732 733 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 734 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 735 736 if "focusType" in iJSON.keys() and iJSON["focusType"]: 737 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 738 739 if "assetType" in iJSON.keys() and iJSON["assetType"]: 740 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 741 742 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 743 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 744 745 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 746 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 747 748 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 749 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 750 751 if "currency" in iJSON.keys(): 752 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 753 754 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 755 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 756 757 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 758 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 759 760 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 761 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 762 763 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 764 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 765 766 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 767 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 768 769 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 770 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 771 772 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 773 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 774 775 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 776 info.append("| Perpetual bond: | Yes |\n") 777 778 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 779 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 780 781 iExt = None 782 if iJSON["type"] == "Bonds": 783 info.extend([ 784 splitLine, 785 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 786 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 787 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 788 iJSON["nominal"]["currency"], 789 )), 790 ]) 791 792 if "floatingCouponFlag" in iJSON.keys(): 793 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 794 795 if "amortizationFlag" in iJSON.keys(): 796 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 797 798 info.append(splitLine) 799 800 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 801 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 802 803 if iJSON["figi"]: 804 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 805 806 info.extend([ 807 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 808 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 809 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 810 ]) 811 812 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 813 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 814 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 815 iJSON["aciValue"]["currency"] 816 ))) 817 818 if "currentPrice" in iJSON.keys(): 819 info.append(splitLine) 820 821 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 822 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 823 824 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 825 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 826 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 827 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 828 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 829 830 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 831 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 832 833 info.extend([ 834 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 835 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 836 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 837 )), 838 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 839 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 840 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 841 )), 842 "| Changes between last deal price and last close | {:<54} |\n".format( 843 "{:.2f}%{}".format( 844 iJSON["currentPrice"]["changes"], 845 " ({}{:.2f} {})".format( 846 "+" if bondChangesDelta > 0 else "", 847 bondChangesDelta, 848 aciCurrency 849 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 850 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 851 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 852 currency 853 ), 854 ) 855 ), 856 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 857 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 858 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 859 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 860 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 861 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 862 )), 863 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 864 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 865 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 866 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 867 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 868 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 869 )), 870 ]) 871 872 if "lot" in iJSON.keys(): 873 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 874 875 if "step" in iJSON.keys() and iJSON["step"] != 0: 876 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 877 878 # Add bond payment calendar: 879 if iJSON["type"] == "Bonds": 880 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 881 info.extend(["\n", strCalendar]) 882 883 infoText += "".join(info) 884 885 if show: 886 uLogger.info("{}".format(infoText)) 887 888 else: 889 uLogger.debug("{}".format(infoText)) 890 891 if self.infoFile is not None: 892 with open(self.infoFile, "w", encoding="UTF-8") as fH: 893 fH.write(infoText) 894 895 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 896 897 return infoText 898 899 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 900 """ 901 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 902 903 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 904 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 905 :return: JSON formatted data with information about instrument. 906 """ 907 tickerJSON = {} 908 if self.moreDebug: 909 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 910 911 if not self.ticker: 912 uLogger.warning("self.ticker variable is not be empty!") 913 914 else: 915 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 916 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 917 raise Exception("Instrument not allowed") 918 919 if not self.iList: 920 self.iList = self.Listing() 921 922 if self.ticker in self.iList["Shares"].keys(): 923 tickerJSON = self.iList["Shares"][self.ticker] 924 if self.moreDebug: 925 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 926 927 elif self.ticker in self.iList["Currencies"].keys(): 928 tickerJSON = self.iList["Currencies"][self.ticker] 929 if self.moreDebug: 930 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 931 932 elif self.ticker in self.iList["Bonds"].keys(): 933 tickerJSON = self.iList["Bonds"][self.ticker] 934 if self.moreDebug: 935 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 936 937 elif self.ticker in self.iList["Etfs"].keys(): 938 tickerJSON = self.iList["Etfs"][self.ticker] 939 if self.moreDebug: 940 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 941 942 elif self.ticker in self.iList["Futures"].keys(): 943 tickerJSON = self.iList["Futures"][self.ticker] 944 if self.moreDebug: 945 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 946 947 if tickerJSON: 948 self.figi = tickerJSON["figi"] 949 950 if requestPrice: 951 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 952 953 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 954 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 955 956 else: 957 tickerJSON["currentPrice"]["changes"] = 0 958 959 if show: 960 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 961 962 else: 963 if show: 964 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 965 966 return tickerJSON 967 968 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 969 """ 970 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 971 972 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 973 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 974 :return: JSON formatted data with information about instrument. 975 """ 976 figiJSON = {} 977 if self.moreDebug: 978 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 979 980 if not self.figi: 981 uLogger.warning("self.figi variable is not be empty!") 982 983 else: 984 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 985 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 986 raise Exception("Instrument not allowed") 987 988 if not self.iList: 989 self.iList = self.Listing() 990 991 for item in self.iList["Shares"].keys(): 992 if self.figi == self.iList["Shares"][item]["figi"]: 993 figiJSON = self.iList["Shares"][item] 994 995 if self.moreDebug: 996 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 997 998 break 999 1000 if not figiJSON: 1001 for item in self.iList["Currencies"].keys(): 1002 if self.figi == self.iList["Currencies"][item]["figi"]: 1003 figiJSON = self.iList["Currencies"][item] 1004 1005 if self.moreDebug: 1006 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1007 1008 break 1009 1010 if not figiJSON: 1011 for item in self.iList["Bonds"].keys(): 1012 if self.figi == self.iList["Bonds"][item]["figi"]: 1013 figiJSON = self.iList["Bonds"][item] 1014 1015 if self.moreDebug: 1016 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1017 1018 break 1019 1020 if not figiJSON: 1021 for item in self.iList["Etfs"].keys(): 1022 if self.figi == self.iList["Etfs"][item]["figi"]: 1023 figiJSON = self.iList["Etfs"][item] 1024 1025 if self.moreDebug: 1026 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1027 1028 break 1029 1030 if not figiJSON: 1031 for item in self.iList["Futures"].keys(): 1032 if self.figi == self.iList["Futures"][item]["figi"]: 1033 figiJSON = self.iList["Futures"][item] 1034 1035 if self.moreDebug: 1036 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1037 1038 break 1039 1040 if figiJSON: 1041 self.figi = figiJSON["figi"] 1042 self.ticker = figiJSON["ticker"] 1043 1044 if requestPrice: 1045 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1046 1047 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1048 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1049 1050 else: 1051 figiJSON["currentPrice"]["changes"] = 0 1052 1053 if show: 1054 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1055 1056 else: 1057 if show: 1058 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1059 1060 return figiJSON 1061 1062 def GetCurrentPrices(self, show: bool = True) -> dict: 1063 """ 1064 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1065 `{"buy": [{"price": 1243.8, "quantity": 193}, 1066 {"price": 1244.0, "quantity": 168}, 1067 {"price": 1244.8, "quantity": 5}, 1068 {"price": 1245.0, "quantity": 61}, 1069 {"price": 1245.4, "quantity": 60}], 1070 "sell": [{"price": 1243.6, "quantity": 8}, 1071 {"price": 1242.6, "quantity": 10}, 1072 {"price": 1242.4, "quantity": 18}, 1073 {"price": 1242.2, "quantity": 50}, 1074 {"price": 1242.0, "quantity": 113}], 1075 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1076 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1077 - sell: list of dicts with Buyers prices, 1078 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1079 - quantity: volume value by current price in lots, 1080 - limitUp: current trade session limit price, maximum, 1081 - limitDown: current trade session limit price, minimum, 1082 - lastPrice: last deal price of the instrument, 1083 - closePrice: previous trade session close price of the instrument. 1084 1085 See also: `SearchByTicker()` and `SearchByFIGI()`. 1086 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1087 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1088 1089 :param show: if `True` then print DOM to log and console. 1090 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1091 If an error occurred then returns an empty record: 1092 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1093 """ 1094 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1095 1096 if self.depth < 1: 1097 uLogger.error("Depth of Market (DOM) must be >=1!") 1098 raise Exception("Incorrect value") 1099 1100 if not (self.ticker or self.figi): 1101 uLogger.error("self.ticker or self.figi variables must be defined!") 1102 raise Exception("Ticker or FIGI required") 1103 1104 if self.ticker and not self.figi: 1105 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1106 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1107 1108 if not self.ticker and self.figi: 1109 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1110 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1111 1112 if not self.figi: 1113 uLogger.error("FIGI is not defined!") 1114 raise Exception("Ticker or FIGI required") 1115 1116 else: 1117 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1118 1119 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1120 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1121 self.body = str({"figi": self.figi, "depth": self.depth}) 1122 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1123 1124 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1125 # list of dicts with sellers orders: 1126 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1127 1128 # list of dicts with buyers orders: 1129 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1130 1131 # max price of instrument at this time: 1132 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1133 1134 # min price of instrument at this time: 1135 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1136 1137 # last price of deal with instrument: 1138 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1139 1140 # last close price of instrument: 1141 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1142 1143 else: 1144 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1145 uLogger.debug("Server response: {}".format(pricesResponse)) 1146 1147 if show: 1148 if prices["buy"] or prices["sell"]: 1149 info = [ 1150 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1151 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1152 self.ticker, 1153 self.figi, 1154 self.depth, 1155 ), 1156 "-" * 60, "\n", 1157 " Orders of Buyers | Orders of Sellers\n", 1158 "-" * 60, "\n", 1159 " Sell prices (volumes) | Buy prices (volumes)\n", 1160 "-" * 60, "\n", 1161 ] 1162 1163 if not prices["buy"]: 1164 info.append(" | No orders!\n") 1165 sumBuy = 0 1166 1167 else: 1168 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1169 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1170 for item in maxMinSorted: 1171 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1172 1173 if not prices["sell"]: 1174 info.append("No orders! |\n") 1175 sumSell = 0 1176 1177 else: 1178 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1179 for item in prices["sell"]: 1180 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1181 1182 info.extend([ 1183 "-" * 60, "\n", 1184 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1185 "-" * 60, "\n", 1186 ]) 1187 1188 infoText = "".join(info) 1189 1190 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1191 1192 else: 1193 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1194 1195 return prices 1196 1197 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1198 """ 1199 This method get and show information about all available broker instruments for current user account. 1200 If `instrumentsFile` string is not empty then also save information to this file. 1201 1202 :param show: if `True` then print results to console, if `False` — print only to file. 1203 :return: multi-lines string with all available broker instruments 1204 """ 1205 if not self.iList: 1206 self.iList = self.Listing() 1207 1208 info = [ 1209 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1210 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1211 ] 1212 1213 # add instruments count by type: 1214 for iType in self.iList.keys(): 1215 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1216 1217 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1218 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1219 1220 # generating info tables with all instruments by type: 1221 for iType in self.iList.keys(): 1222 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1223 1224 for instrument in self.iList[iType].keys(): 1225 iName = self.iList[iType][instrument]["name"] # instrument's name 1226 if len(iName) > 57: 1227 iName = "{}...".format(iName[:54]) # right trim for a long string 1228 1229 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1230 self.iList[iType][instrument]["ticker"], 1231 iName, 1232 self.iList[iType][instrument]["figi"], 1233 self.iList[iType][instrument]["currency"], 1234 self.iList[iType][instrument]["lot"], 1235 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1236 )) 1237 1238 infoText = "".join(info) 1239 1240 if show: 1241 uLogger.info(infoText) 1242 1243 if self.instrumentsFile: 1244 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1245 fH.write(infoText) 1246 1247 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1248 1249 return infoText 1250 1251 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1252 """ 1253 This method search and show information about instruments by part of its ticker, FIGI or name. 1254 If `searchResultsFile` string is not empty then also save information to this file. 1255 1256 :param pattern: string with part of ticker, FIGI or instrument's name. 1257 :param show: if `True` then print results to console, if `False` — return list of result only. 1258 :return: list of dictionaries with all found instruments. 1259 """ 1260 if not self.iList: 1261 self.iList = self.Listing() 1262 1263 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1264 compiledPattern = re.compile(pattern, re.IGNORECASE) 1265 1266 for iType in self.iList: 1267 for instrument in self.iList[iType].values(): 1268 searchResult = compiledPattern.search(" ".join( 1269 [instrument["ticker"], instrument["figi"], instrument["name"]] 1270 )) 1271 1272 if searchResult: 1273 searchResults[iType][instrument["ticker"]] = instrument 1274 1275 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1276 info = [ 1277 "# Search results\n\n", 1278 "* **Search pattern:** [{}]\n".format(pattern), 1279 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1280 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1281 ] 1282 infoShort = info[:] 1283 1284 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1285 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1286 skippedLine = "| ... | ... | ... | ... |\n" 1287 1288 if resultsLen == 0: 1289 info.append("\nNo results\n") 1290 infoShort.append("\nNo results\n") 1291 uLogger.warning("No results. Try changing your search pattern.") 1292 1293 else: 1294 for iType in searchResults: 1295 iTypeValuesCount = len(searchResults[iType].values()) 1296 if iTypeValuesCount > 0: 1297 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1298 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1299 1300 for instrument in searchResults[iType].values(): 1301 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1302 instrument["type"], 1303 instrument["ticker"], 1304 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1305 instrument["figi"], 1306 )) 1307 1308 if iTypeValuesCount <= 5: 1309 infoShort.extend(info[-iTypeValuesCount:]) 1310 1311 else: 1312 infoShort.extend(info[-5:]) 1313 infoShort.append(skippedLine) 1314 1315 infoText = "".join(info) 1316 infoTextShort = "".join(infoShort) 1317 1318 if show: 1319 uLogger.info(infoTextShort) 1320 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1321 1322 if self.searchResultsFile: 1323 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1324 fH.write(infoText) 1325 1326 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1327 1328 return searchResults 1329 1330 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1331 """ 1332 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1333 1334 :param instruments: list of strings with tickers or FIGIs. 1335 :return: list with unique instrument FIGIs only. 1336 """ 1337 requestedInstruments = [] 1338 for iName in instruments: 1339 if iName not in self.aliases.keys(): 1340 if iName not in requestedInstruments: 1341 requestedInstruments.append(iName) 1342 1343 else: 1344 if iName not in requestedInstruments: 1345 if self.aliases[iName] not in requestedInstruments: 1346 requestedInstruments.append(self.aliases[iName]) 1347 1348 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1349 1350 onlyUniqueFIGIs = [] 1351 for iName in requestedInstruments: 1352 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1353 continue 1354 1355 self.ticker = iName 1356 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1357 1358 if not iData: 1359 self.ticker = "" 1360 self.figi = iName 1361 1362 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1363 1364 if not iData: 1365 self.figi = "" 1366 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1367 1368 if iData and iData["figi"] not in onlyUniqueFIGIs: 1369 onlyUniqueFIGIs.append(iData["figi"]) 1370 1371 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1372 1373 return onlyUniqueFIGIs 1374 1375 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1376 """ 1377 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1378 1379 See limits: https://tinkoff.github.io/investAPI/limits/ 1380 1381 If `pricesFile` string is not empty then also save information to this file. 1382 1383 :param instruments: list of strings with tickers or FIGIs. 1384 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1385 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1386 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1387 """ 1388 if instruments is None or not instruments: 1389 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1390 raise Exception("Ticker or FIGI required") 1391 1392 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1393 1394 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1395 1396 iList = [] # trying to get info and current prices about all unique instruments: 1397 for self.figi in onlyUniqueFIGIs: 1398 iData = self.SearchByFIGI(requestPrice=True) 1399 iList.append(iData) 1400 1401 self.ShowListOfPrices(iList, show) 1402 1403 return iList 1404 1405 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1406 """ 1407 Show table contains current prices of given instruments. 1408 1409 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1410 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1411 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1412 :return: multilines text in Markdown format as a table contains current prices. 1413 """ 1414 infoText = "" 1415 1416 if show or self.pricesFile: 1417 info = [ 1418 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1419 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1420 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1421 ] 1422 1423 for item in iList: 1424 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1425 item["ticker"], 1426 item["figi"], 1427 item["type"], 1428 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1429 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1430 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1431 "{} / {}".format( 1432 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1433 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1434 ), 1435 "{} / {}".format( 1436 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1437 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1438 ), 1439 item["currency"], 1440 )) 1441 1442 infoText = "".join(info) 1443 1444 if show: 1445 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1446 1447 if self.pricesFile: 1448 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1449 fH.write(infoText) 1450 1451 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1452 1453 return infoText 1454 1455 def RequestTradingStatus(self) -> dict: 1456 """ 1457 Requesting trading status for the instrument defined by `figi` variable. 1458 1459 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1460 1461 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1462 1463 :return: dictionary with trading status attributes. Response example: 1464 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1465 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1466 """ 1467 if self.figi is None or not self.figi: 1468 uLogger.error("Variable `figi` must be defined for using this method!") 1469 raise Exception("FIGI required") 1470 1471 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1472 1473 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1474 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1475 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1476 1477 if self.moreDebug: 1478 uLogger.debug("Records about current trading status successfully received") 1479 1480 return tradingStatus 1481 1482 def RequestPortfolio(self) -> dict: 1483 """ 1484 Requesting actual user's portfolio for current `accountId`. 1485 1486 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1487 1488 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1489 1490 :return: dictionary with user's portfolio. 1491 """ 1492 if self.accountId is None or not self.accountId: 1493 uLogger.error("Variable `accountId` must be defined for using this method!") 1494 raise Exception("Account ID required") 1495 1496 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1497 1498 self.body = str({"accountId": self.accountId}) 1499 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1500 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1501 1502 if self.moreDebug: 1503 uLogger.debug("Records about user's portfolio successfully received") 1504 1505 return rawPortfolio 1506 1507 def RequestPositions(self) -> dict: 1508 """ 1509 Requesting open positions by currencies and instruments for current `accountId`. 1510 1511 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1512 1513 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1514 1515 :return: dictionary with open positions by instruments. 1516 """ 1517 if self.accountId is None or not self.accountId: 1518 uLogger.error("Variable `accountId` must be defined for using this method!") 1519 raise Exception("Account ID required") 1520 1521 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1522 1523 self.body = str({"accountId": self.accountId}) 1524 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1525 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1526 1527 if self.moreDebug: 1528 uLogger.debug("Records about current open positions successfully received") 1529 1530 return rawPositions 1531 1532 def RequestPendingOrders(self) -> list: 1533 """ 1534 Requesting current actual pending orders for current `accountId`. 1535 1536 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1537 1538 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1539 1540 :return: list of dictionaries with pending orders. 1541 """ 1542 if self.accountId is None or not self.accountId: 1543 uLogger.error("Variable `accountId` must be defined for using this method!") 1544 raise Exception("Account ID required") 1545 1546 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1547 1548 self.body = str({"accountId": self.accountId}) 1549 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1550 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1551 1552 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1553 1554 return rawOrders 1555 1556 def RequestStopOrders(self) -> list: 1557 """ 1558 Requesting current actual stop orders for current `accountId`. 1559 1560 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1561 1562 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1563 1564 :return: list of dictionaries with stop orders. 1565 """ 1566 if self.accountId is None or not self.accountId: 1567 uLogger.error("Variable `accountId` must be defined for using this method!") 1568 raise Exception("Account ID required") 1569 1570 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1571 1572 self.body = str({"accountId": self.accountId}) 1573 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1574 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1575 1576 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1577 1578 return rawStopOrders 1579 1580 def Overview(self, show: bool = False, details: str = "full") -> dict: 1581 """ 1582 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1583 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1584 and `overviewBondsCalendarFile` are defined then also save information to file. 1585 1586 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1587 many requests about the state of the portfolio, and then, based on the received data, a large number 1588 of calculation and statistics are collected. 1589 1590 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1591 :param details: how detailed should the information be? 1592 - `full` — shows full available information about portfolio status (by default), 1593 - `positions` — shows only open positions, 1594 - `orders` — shows only sections of open limits and stop orders. 1595 - `digest` — show a short digest of the portfolio status, 1596 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1597 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1598 :return: dictionary with client's raw portfolio and some statistics. 1599 """ 1600 if self.accountId is None or not self.accountId: 1601 uLogger.error("Variable `accountId` must be defined for using this method!") 1602 raise Exception("Account ID required") 1603 1604 view = { 1605 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1606 "headers": {}, # list of dictionaries, response headers without "positions" section 1607 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1608 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1609 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1610 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1611 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1612 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1613 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1614 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1615 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1616 }, 1617 "stat": { # --- some statistics calculated using "raw" sections: 1618 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1619 "availableRUB": 0., # available rubles (without other currencies) 1620 "blockedRUB": 0., # blocked sum in Russian Rouble 1621 "totalChangesRUB": 0., # changes for all open trades in RUB 1622 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1623 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1624 "sharesCostRUB": 0., # costs of all shares in RUB 1625 "bondsCostRUB": 0., # costs of all bonds in RUB 1626 "etfsCostRUB": 0., # costs of all etfs in RUB 1627 "futuresCostRUB": 0., # costs of all futures in RUB 1628 "Currencies": [], # list of dictionaries of all currencies statistics 1629 "Shares": [], # list of dictionaries of all shares statistics 1630 "Bonds": [], # list of dictionaries of all bonds statistics 1631 "Etfs": [], # list of dictionaries of all etfs statistics 1632 "Futures": [], # list of dictionaries of all futures statistics 1633 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1634 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1635 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1636 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1637 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1638 }, 1639 "analytics": { # --- some analytics of portfolio: 1640 "distrByAssets": {}, # portfolio distribution by assets 1641 "distrByCompanies": {}, # portfolio distribution by companies 1642 "distrBySectors": {}, # portfolio distribution by sectors 1643 "distrByCurrencies": {}, # portfolio distribution by currencies 1644 "distrByCountries": {}, # portfolio distribution by countries 1645 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1646 } 1647 } 1648 1649 details = details.lower() 1650 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1651 if details not in availableDetails: 1652 details = "full" 1653 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1654 1655 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1656 1657 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1658 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1659 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1660 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1661 1662 # save response headers without "positions" section: 1663 for key in portfolioResponse.keys(): 1664 if key != "positions": 1665 view["raw"]["headers"][key] = portfolioResponse[key] 1666 1667 else: 1668 continue 1669 1670 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1671 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1672 for item in portfolioResponse["positions"]: 1673 if item["instrumentType"] == "currency": 1674 self.figi = item["figi"] 1675 curr = self.SearchByFIGI(requestPrice=False) 1676 1677 # current price of currency in RUB: 1678 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1679 "name": curr["name"], 1680 "currentPrice": NanoToFloat( 1681 item["currentPrice"]["units"], 1682 item["currentPrice"]["nano"] 1683 ), 1684 } 1685 1686 view["raw"]["Currencies"].append(item) 1687 1688 elif item["instrumentType"] == "share": 1689 view["raw"]["Shares"].append(item) 1690 1691 elif item["instrumentType"] == "bond": 1692 view["raw"]["Bonds"].append(item) 1693 1694 elif item["instrumentType"] == "etf": 1695 view["raw"]["Etfs"].append(item) 1696 1697 elif item["instrumentType"] == "futures": 1698 view["raw"]["Futures"].append(item) 1699 1700 else: 1701 continue 1702 1703 # how many volume of currencies (by ISO currency name) are blocked: 1704 for item in view["raw"]["positions"]["blocked"]: 1705 blocked = NanoToFloat(item["units"], item["nano"]) 1706 if blocked > 0: 1707 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1708 1709 # how many volume of instruments (by FIGI) are blocked: 1710 for item in view["raw"]["positions"]["securities"]: 1711 blocked = int(item["blocked"]) 1712 if blocked > 0: 1713 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1714 1715 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1716 1717 if "rub" in allBlocked.keys(): 1718 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1719 1720 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1721 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1722 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1723 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1724 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1725 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1726 view["stat"]["portfolioCostRUB"] = sum([ 1727 view["stat"]["allCurrenciesCostRUB"], 1728 view["stat"]["sharesCostRUB"], 1729 view["stat"]["bondsCostRUB"], 1730 view["stat"]["etfsCostRUB"], 1731 view["stat"]["futuresCostRUB"], 1732 ]) 1733 1734 # --- calculating some portfolio statistics: 1735 byComp = {} # distribution by companies 1736 bySect = {} # distribution by sectors 1737 byCurr = {} # distribution by currencies (include RUB) 1738 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1739 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1740 1741 for item in portfolioResponse["positions"]: 1742 self.figi = item["figi"] 1743 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1744 1745 if instrument: 1746 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1747 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1748 1749 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1750 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1751 1752 else: 1753 blocked = 0 1754 1755 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1756 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1757 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1758 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1759 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1760 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1761 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1762 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1763 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1764 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1765 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1766 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1767 1768 statData = { 1769 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1770 "ticker": instrument["ticker"], # ticker by FIGI 1771 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1772 "volume": volume, # available volume of instrument 1773 "lots": lots, # volume in lots of instrument 1774 "direction": direction, # direction of an instrument's position: short or long 1775 "blocked": blocked, # blocked volume of currency or instrument 1776 "currentPrice": curPrice, # current instrument's price in basic asset 1777 "average": average, # current average position price 1778 "cost": cost, # current cost of all volume of instrument in basic asset 1779 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1780 "costRUB": costRUB, # cost of instrument in ruble 1781 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1782 "profit": profit, # expected profit at current moment 1783 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1784 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1785 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1786 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1787 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1788 "step": instrument["step"], # minimum price increment 1789 } 1790 1791 # adding distribution by unique countries: 1792 if statData["country"] not in byCountry.keys(): 1793 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1794 1795 else: 1796 byCountry[statData["country"]]["cost"] += costRUB 1797 byCountry[statData["country"]]["percent"] += percentCostRUB 1798 1799 if item["instrumentType"] != "currency": 1800 # adding distribution by unique companies: 1801 if statData["name"]: 1802 if statData["name"] not in byComp.keys(): 1803 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1804 1805 else: 1806 byComp[statData["name"]]["cost"] += costRUB 1807 byComp[statData["name"]]["percent"] += percentCostRUB 1808 1809 # adding distribution by unique sectors: 1810 if statData["sector"] not in bySect.keys(): 1811 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1812 1813 else: 1814 bySect[statData["sector"]]["cost"] += costRUB 1815 bySect[statData["sector"]]["percent"] += percentCostRUB 1816 1817 # adding distribution by unique currencies: 1818 if currency not in byCurr.keys(): 1819 byCurr[currency] = { 1820 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1821 "cost": costRUB, 1822 "percent": percentCostRUB 1823 } 1824 1825 else: 1826 byCurr[currency]["cost"] += costRUB 1827 byCurr[currency]["percent"] += percentCostRUB 1828 1829 # saving statistics for every instrument: 1830 if item["instrumentType"] == "currency": 1831 view["stat"]["Currencies"].append(statData) 1832 1833 # update dict with free funds for trading (total - blocked) by currencies 1834 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1835 view["stat"]["funds"][currency] = { 1836 "total": volume, 1837 "totalCostRUB": costRUB, # total volume cost in rubles 1838 "free": volume - blocked, 1839 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1840 } 1841 1842 elif item["instrumentType"] == "share": 1843 view["stat"]["Shares"].append(statData) 1844 1845 elif item["instrumentType"] == "bond": 1846 view["stat"]["Bonds"].append(statData) 1847 1848 elif item["instrumentType"] == "etf": 1849 view["stat"]["Etfs"].append(statData) 1850 1851 elif item["instrumentType"] == "Futures": 1852 view["stat"]["Futures"].append(statData) 1853 1854 else: 1855 continue 1856 1857 # total changes in Russian Ruble: 1858 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1859 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1860 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1861 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1862 view["stat"]["funds"]["rub"] = { 1863 "total": view["stat"]["availableRUB"], 1864 "totalCostRUB": view["stat"]["availableRUB"], 1865 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1866 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1867 } 1868 1869 # --- pending orders sector data: 1870 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending orders to avoid many times price requests 1871 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1872 1873 for item in view["raw"]["orders"]: 1874 self.figi = item["figi"] 1875 1876 if item["figi"] not in uniquePendingOrdersFIGIs: 1877 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1878 1879 uniquePendingOrdersFIGIs.append(item["figi"]) 1880 uniquePendingOrders[item["figi"]] = instrument 1881 1882 else: 1883 instrument = uniquePendingOrders[item["figi"]] 1884 1885 if instrument: 1886 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1887 orderType = TKS_ORDER_TYPES[item["orderType"]] 1888 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1889 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1890 1891 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1892 if item["direction"] == "ORDER_DIRECTION_BUY": 1893 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1894 1895 else: 1896 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1897 1898 # requested price for order execution: 1899 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1900 1901 # necessary changes in percent to reach target from current price: 1902 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1903 1904 view["stat"]["orders"].append({ 1905 "orderID": item["orderId"], # orderId number parameter of current order 1906 "figi": item["figi"], # FIGI identification 1907 "ticker": instrument["ticker"], # ticker name by FIGI 1908 "lotsRequested": item["lotsRequested"], # requested lots value 1909 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1910 "currentPrice": lastPrice, # current instrument's price for defined action 1911 "targetPrice": target, # requested price for order execution in base currency 1912 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1913 "percentChanges": changes, # changes in percent to target from current price 1914 "currency": item["currency"], # instrument's currency name 1915 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1916 "type": orderType, # type of order from TKS_ORDER_TYPES 1917 "status": orderState, # order status from TKS_ORDER_STATES 1918 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1919 }) 1920 1921 # --- stop orders sector data: 1922 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1923 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1924 1925 for item in view["raw"]["stopOrders"]: 1926 self.figi = item["figi"] 1927 1928 if item["figi"] not in uniqueStopOrdersFIGIs: 1929 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1930 1931 uniqueStopOrdersFIGIs.append(item["figi"]) 1932 uniqueStopOrders[item["figi"]] = instrument 1933 1934 else: 1935 instrument = uniqueStopOrders[item["figi"]] 1936 1937 if instrument: 1938 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1939 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1940 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1941 1942 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1943 if "expirationTime" in item.keys(): 1944 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1945 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1946 1947 else: 1948 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1949 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1950 1951 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1952 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1953 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1954 1955 else: 1956 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1957 1958 # requested price when stop-order executed: 1959 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1960 1961 # price for limit-order, set up when stop-order executed: 1962 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1963 1964 # necessary changes in percent to reach target from current price: 1965 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1966 1967 view["stat"]["stopOrders"].append({ 1968 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1969 "figi": item["figi"], # FIGI identification 1970 "ticker": instrument["ticker"], # ticker name by FIGI 1971 "lotsRequested": item["lotsRequested"], # requested lots value 1972 "currentPrice": lastPrice, # current instrument's price for defined action 1973 "targetPrice": target, # requested price for stop-order execution in base currency 1974 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1975 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1976 "percentChanges": changes, # changes in percent to target from current price 1977 "currency": item["currency"], # instrument's currency name 1978 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1979 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1980 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1981 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1982 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 1983 }) 1984 1985 # --- calculating data for analytics section: 1986 # portfolio distribution by assets: 1987 view["analytics"]["distrByAssets"] = { 1988 "Ruble": { 1989 "uniques": 1, 1990 "cost": view["stat"]["availableRUB"], 1991 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1992 }, 1993 "Currencies": { 1994 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 1995 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 1996 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1997 }, 1998 "Shares": { 1999 "uniques": len(view["stat"]["Shares"]), 2000 "cost": view["stat"]["sharesCostRUB"], 2001 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2002 }, 2003 "Bonds": { 2004 "uniques": len(view["stat"]["Bonds"]), 2005 "cost": view["stat"]["bondsCostRUB"], 2006 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2007 }, 2008 "Etfs": { 2009 "uniques": len(view["stat"]["Etfs"]), 2010 "cost": view["stat"]["etfsCostRUB"], 2011 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2012 }, 2013 "Futures": { 2014 "uniques": len(view["stat"]["Futures"]), 2015 "cost": view["stat"]["futuresCostRUB"], 2016 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2017 }, 2018 } 2019 2020 # portfolio distribution by companies: 2021 view["analytics"]["distrByCompanies"]["All money cash"] = { 2022 "ticker": "", 2023 "cost": view["stat"]["allCurrenciesCostRUB"], 2024 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2025 } 2026 view["analytics"]["distrByCompanies"].update(byComp) 2027 2028 # portfolio distribution by sectors: 2029 view["analytics"]["distrBySectors"]["All money cash"] = { 2030 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2031 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2032 } 2033 view["analytics"]["distrBySectors"].update(bySect) 2034 2035 # portfolio distribution by currencies: 2036 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2037 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2038 2039 if self.moreDebug: 2040 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2041 2042 view["analytics"]["distrByCurrencies"].update(byCurr) 2043 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2044 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2045 2046 # portfolio distribution by countries: 2047 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2048 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2049 2050 if self.moreDebug: 2051 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2052 2053 view["analytics"]["distrByCountries"].update(byCountry) 2054 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2055 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2056 2057 # --- Prepare text statistics overview in human-readable: 2058 if show: 2059 # Whatever the value `details`, header not changes: 2060 info = [ 2061 "# Client's portfolio\n\n", 2062 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2063 "* **Account ID:** [{}]\n".format(self.accountId), 2064 ] 2065 2066 if details in ["full", "positions", "digest"]: 2067 info.extend([ 2068 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2069 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2070 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2071 view["stat"]["totalChangesRUB"], 2072 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2073 view["stat"]["totalChangesPercentRUB"], 2074 ), 2075 ]) 2076 2077 if details in ["full", "positions"]: 2078 info.extend([ 2079 "## Open positions\n\n", 2080 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2081 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2082 "| Ruble | {:>31} | | | | | |\n".format( 2083 "{:.2f} ({:.2f}) rub".format( 2084 view["stat"]["availableRUB"], 2085 view["stat"]["blockedRUB"], 2086 ) 2087 ) 2088 ]) 2089 2090 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2091 return [ 2092 "| | | | | | | |\n", 2093 "| {:<27} | | | | | {:>19} | |\n".format( 2094 noTradeStr if noTradeStr else typeStr, 2095 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2096 ), 2097 ] 2098 2099 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2100 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2101 "{} [{}]".format(data["ticker"], data["figi"]), 2102 "{:.2f} ({:.2f}) {}".format( 2103 data["volume"], 2104 data["blocked"], 2105 data["currency"], 2106 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2107 data["volume"], 2108 data["blocked"], 2109 ), 2110 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2111 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2112 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2113 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2114 "{}{:.2f} {} ({}{:.2f}%)".format( 2115 "+" if data["profit"] > 0 else "", 2116 data["profit"], data["baseCurrencyName"], 2117 "+" if data["percentProfit"] > 0 else "", 2118 data["percentProfit"], 2119 ), 2120 ) 2121 2122 # --- Show currencies section: 2123 if view["stat"]["Currencies"]: 2124 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2125 for item in view["stat"]["Currencies"]: 2126 info.append(_InfoStr(item, showCurrencyName=True)) 2127 2128 else: 2129 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2130 2131 # --- Show shares section: 2132 if view["stat"]["Shares"]: 2133 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2134 2135 for item in view["stat"]["Shares"]: 2136 info.append(_InfoStr(item)) 2137 2138 else: 2139 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2140 2141 # --- Show bonds section: 2142 if view["stat"]["Bonds"]: 2143 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2144 2145 for item in view["stat"]["Bonds"]: 2146 info.append(_InfoStr(item)) 2147 2148 else: 2149 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2150 2151 # --- Show etfs section: 2152 if view["stat"]["Etfs"]: 2153 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2154 2155 for item in view["stat"]["Etfs"]: 2156 info.append(_InfoStr(item)) 2157 2158 else: 2159 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2160 2161 # --- Show futures section: 2162 if view["stat"]["Futures"]: 2163 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2164 2165 for item in view["stat"]["Futures"]: 2166 info.append(_InfoStr(item)) 2167 2168 else: 2169 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2170 2171 if details in ["full", "orders"]: 2172 # --- Show pending orders section: 2173 if view["stat"]["orders"]: 2174 info.extend([ 2175 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2176 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2177 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2178 ]) 2179 2180 for item in view["stat"]["orders"]: 2181 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2182 "{} [{}]".format(item["ticker"], item["figi"]), 2183 item["orderID"], 2184 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2185 "{} {} ({}{:.2f}%)".format( 2186 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2187 item["baseCurrencyName"], 2188 "+" if item["percentChanges"] > 0 else "", 2189 float(item["percentChanges"]), 2190 ), 2191 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2192 item["action"], 2193 item["type"], 2194 item["date"], 2195 )) 2196 2197 else: 2198 info.append("\n## Total pending limit-orders: 0\n") 2199 2200 # --- Show stop orders section: 2201 if view["stat"]["stopOrders"]: 2202 info.extend([ 2203 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2204 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2205 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2206 ]) 2207 2208 for item in view["stat"]["stopOrders"]: 2209 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2210 "{} [{}]".format(item["ticker"], item["figi"]), 2211 item["orderID"], 2212 item["lotsRequested"], 2213 "{} {} ({}{:.2f}%)".format( 2214 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2215 item["baseCurrencyName"], 2216 "+" if item["percentChanges"] > 0 else "", 2217 float(item["percentChanges"]), 2218 ), 2219 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2220 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2221 item["action"], 2222 item["type"], 2223 item["expType"], 2224 item["createDate"], 2225 item["expDate"], 2226 )) 2227 2228 else: 2229 info.append("\n## Total stop-orders: 0\n") 2230 2231 if details in ["full", "analytics"]: 2232 # -- Show analytics section: 2233 if view["stat"]["portfolioCostRUB"] > 0: 2234 info.extend([ 2235 "\n# Analytics\n" 2236 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2237 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2238 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2239 view["stat"]["totalChangesRUB"], 2240 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2241 view["stat"]["totalChangesPercentRUB"], 2242 ), 2243 "\n## Portfolio distribution by assets\n" 2244 "\n| Type | Uniques | Percent | Current cost |\n", 2245 "|------------------------------------|---------|---------|--------------------|\n", 2246 ]) 2247 2248 for key in view["analytics"]["distrByAssets"].keys(): 2249 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2250 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2251 key, 2252 view["analytics"]["distrByAssets"][key]["uniques"], 2253 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2254 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2255 )) 2256 2257 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2258 2259 info.extend([ 2260 "\n## Portfolio distribution by companies\n" 2261 "\n| Company | Percent | Current cost |\n", 2262 aSepLine, 2263 ]) 2264 2265 for company in view["analytics"]["distrByCompanies"].keys(): 2266 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2267 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2268 "{}{}".format( 2269 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2270 company, 2271 ), 2272 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2273 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2274 )) 2275 2276 info.extend([ 2277 "\n## Portfolio distribution by sectors\n" 2278 "\n| Sector | Percent | Current cost |\n", 2279 aSepLine, 2280 ]) 2281 2282 for sector in view["analytics"]["distrBySectors"].keys(): 2283 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2284 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2285 sector, 2286 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2287 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2288 )) 2289 2290 info.extend([ 2291 "\n## Portfolio distribution by currencies\n" 2292 "\n| Instruments currencies | Percent | Current cost |\n", 2293 aSepLine, 2294 ]) 2295 2296 for curr in view["analytics"]["distrByCurrencies"].keys(): 2297 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2298 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2299 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2300 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2301 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2302 )) 2303 2304 info.extend([ 2305 "\n## Portfolio distribution by countries\n" 2306 "\n| Assets by country | Percent | Current cost |\n", 2307 aSepLine, 2308 ]) 2309 2310 for country in view["analytics"]["distrByCountries"].keys(): 2311 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2312 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2313 country, 2314 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2315 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2316 )) 2317 2318 if details in ["full", "calendar"]: 2319 # -- Show bonds payment calendar section: 2320 if view["stat"]["Bonds"]: 2321 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2322 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2323 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2324 2325 else: 2326 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2327 2328 infoText = "".join(info) 2329 2330 uLogger.info(infoText) 2331 2332 if details == "full" and self.overviewFile: 2333 filename = self.overviewFile 2334 2335 elif details == "digest" and self.overviewDigestFile: 2336 filename = self.overviewDigestFile 2337 2338 elif details == "positions" and self.overviewPositionsFile: 2339 filename = self.overviewPositionsFile 2340 2341 elif details == "orders" and self.overviewOrdersFile: 2342 filename = self.overviewOrdersFile 2343 2344 elif details == "analytics" and self.overviewAnalyticsFile: 2345 filename = self.overviewAnalyticsFile 2346 2347 elif details == "calendar" and self.overviewBondsCalendarFile: 2348 filename = self.overviewBondsCalendarFile 2349 2350 else: 2351 filename = "" 2352 2353 if filename: 2354 with open(filename, "w", encoding="UTF-8") as fH: 2355 fH.write(infoText) 2356 2357 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2358 2359 return view 2360 2361 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2362 """ 2363 Returns history operations between two given dates for current `accountId`. 2364 If `reportFile` string is not empty then also save human-readable report. 2365 Shows some statistical data of closed positions. 2366 2367 :param start: see docstring in `GetDatesAsString()` method 2368 :param end: see docstring in `GetDatesAsString()` method 2369 :param show: if `True` then also prints all records to the console. 2370 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2371 :return: original list of dictionaries with history of deals records from API ("operations" key): 2372 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2373 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2374 """ 2375 if self.accountId is None or not self.accountId: 2376 uLogger.error("Variable `accountId` must be defined for using this method!") 2377 raise Exception("Account ID required") 2378 2379 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2380 2381 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2382 2383 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2384 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2385 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2386 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2387 customStat = {} # custom statistics in additional to responseJSON 2388 2389 # --- output report in human-readable format: 2390 if show or self.reportFile: 2391 splitLine1 = "| | | | | |\n" # Summary section 2392 splitLine2 = "| | | | | | | | |\n" # Operations section 2393 nextDay = "" 2394 2395 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2396 2397 if len(ops) > 0: 2398 customStat = { 2399 "opsCount": 0, # total operations count 2400 "buyCount": 0, # buy operations 2401 "sellCount": 0, # sell operations 2402 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2403 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2404 "payIn": {"rub": 0.}, # Deposit brokerage account 2405 "payOut": {"rub": 0.}, # Withdrawals 2406 "divs": {"rub": 0.}, # Dividends income 2407 "coupons": {"rub": 0.}, # Coupon's income 2408 "brokerCom": {"rub": 0.}, # Service commissions 2409 "serviceCom": {"rub": 0.}, # Service commissions 2410 "marginCom": {"rub": 0.}, # Margin commissions 2411 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2412 } 2413 2414 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2415 for item in ops: 2416 if item["state"] == "OPERATION_STATE_EXECUTED": 2417 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2418 2419 # count buy operations: 2420 if "_BUY" in item["operationType"]: 2421 customStat["buyCount"] += 1 2422 2423 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2424 customStat["buyTotal"][item["payment"]["currency"]] += payment 2425 2426 else: 2427 customStat["buyTotal"][item["payment"]["currency"]] = payment 2428 2429 # count sell operations: 2430 elif "_SELL" in item["operationType"]: 2431 customStat["sellCount"] += 1 2432 2433 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2434 customStat["sellTotal"][item["payment"]["currency"]] += payment 2435 2436 else: 2437 customStat["sellTotal"][item["payment"]["currency"]] = payment 2438 2439 # count incoming operations: 2440 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2441 if item["payment"]["currency"] in customStat["payIn"].keys(): 2442 customStat["payIn"][item["payment"]["currency"]] += payment 2443 2444 else: 2445 customStat["payIn"][item["payment"]["currency"]] = payment 2446 2447 # count withdrawals operations: 2448 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2449 if item["payment"]["currency"] in customStat["payOut"].keys(): 2450 customStat["payOut"][item["payment"]["currency"]] += payment 2451 2452 else: 2453 customStat["payOut"][item["payment"]["currency"]] = payment 2454 2455 # count dividends income: 2456 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2457 if item["payment"]["currency"] in customStat["divs"].keys(): 2458 customStat["divs"][item["payment"]["currency"]] += payment 2459 2460 else: 2461 customStat["divs"][item["payment"]["currency"]] = payment 2462 2463 # count coupon's income: 2464 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2465 if item["payment"]["currency"] in customStat["coupons"].keys(): 2466 customStat["coupons"][item["payment"]["currency"]] += payment 2467 2468 else: 2469 customStat["coupons"][item["payment"]["currency"]] = payment 2470 2471 # count broker commissions: 2472 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2473 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2474 customStat["brokerCom"][item["payment"]["currency"]] += payment 2475 2476 else: 2477 customStat["brokerCom"][item["payment"]["currency"]] = payment 2478 2479 # count service commissions: 2480 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2481 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2482 customStat["serviceCom"][item["payment"]["currency"]] += payment 2483 2484 else: 2485 customStat["serviceCom"][item["payment"]["currency"]] = payment 2486 2487 # count margin commissions: 2488 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2489 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2490 customStat["marginCom"][item["payment"]["currency"]] += payment 2491 2492 else: 2493 customStat["marginCom"][item["payment"]["currency"]] = payment 2494 2495 # count withholding taxes: 2496 elif "_TAX" in item["operationType"]: 2497 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2498 customStat["allTaxes"][item["payment"]["currency"]] += payment 2499 2500 else: 2501 customStat["allTaxes"][item["payment"]["currency"]] = payment 2502 2503 else: 2504 continue 2505 2506 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2507 2508 # --- view "Actions" lines: 2509 info.extend([ 2510 "| Report sections | | | | |\n", 2511 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2512 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2513 "| | Buy: {:<22} | {:<28} | | |\n".format( 2514 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2515 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2516 ), 2517 "| | Sell: {:<21} | {:<28} | | |\n".format( 2518 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2519 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2520 ), 2521 ]) 2522 2523 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2524 for key in opsKeys: 2525 if key == "rub": 2526 continue 2527 2528 info.extend([ 2529 "| | | {:<28} | | |\n".format( 2530 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2531 ), 2532 "| | | {:<28} | | |\n".format( 2533 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2534 ), 2535 ]) 2536 2537 info.append(splitLine1) 2538 2539 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2540 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2541 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2542 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2543 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2544 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2545 ) 2546 2547 # --- view "Payments" lines: 2548 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2549 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2550 2551 for key in paymentsKeys: 2552 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2553 2554 info.append(splitLine1) 2555 2556 # --- view "Commissions and taxes" lines: 2557 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2558 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2559 2560 for key in comKeys: 2561 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2562 2563 info.append(splitLine1) 2564 2565 info.extend([ 2566 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2567 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2568 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2569 ]) 2570 2571 else: 2572 info.append("Broker returned no operations during this period\n") 2573 2574 # --- view "Operations" section: 2575 for item in ops: 2576 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2577 continue 2578 2579 else: 2580 self.figi = item["figi"] if item["figi"] else "" 2581 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2582 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2583 2584 # group of deals during one day: 2585 if nextDay and item["date"].split("T")[0] != nextDay: 2586 info.append(splitLine2) 2587 nextDay = "" 2588 2589 else: 2590 nextDay = item["date"].split("T")[0] # saving current day for splitting 2591 2592 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2593 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2594 self.figi if self.figi else "—", 2595 instrument["ticker"] if instrument else "—", 2596 instrument["type"] if instrument else "—", 2597 item["quantity"] if int(item["quantity"]) > 0 else "—", 2598 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2599 TKS_OPERATION_STATES[item["state"]], 2600 TKS_OPERATION_TYPES[item["operationType"]], 2601 )) 2602 2603 infoText = "".join(info) 2604 2605 if show: 2606 if self.moreDebug: 2607 uLogger.debug("Records about history of a client's operations successfully received") 2608 2609 uLogger.info(infoText) 2610 2611 if self.reportFile: 2612 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2613 fH.write(infoText) 2614 2615 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2616 2617 return ops, customStat 2618 2619 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2620 """ 2621 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2622 2623 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2624 Warning! Broker server used ISO UTC time by default. 2625 2626 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2627 Also, `historyFile` used to update history with `onlyMissing` parameter. 2628 2629 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2630 2631 :param start: see docstring in `GetDatesAsString()` method. 2632 :param end: see docstring in `GetDatesAsString()` method. 2633 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2634 `"hour"`, `"day"`. Default: `"hour"`. 2635 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2636 False by default. Warning! History appends only from last candle to current time 2637 with always update last candle! 2638 :param csvSep: separator if csv-file is used, `,` by default. 2639 :param show: if `True` then also prints Pandas DataFrame to the console. 2640 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2641 `["date", "time", "open", "high", "low", "close", "volume"]`. 2642 """ 2643 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2644 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2645 history = None # empty pandas object for history 2646 2647 if interval not in TKS_CANDLE_INTERVALS.keys(): 2648 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2649 raise Exception("Incorrect value") 2650 2651 if not (self.ticker or self.figi): 2652 uLogger.error("Ticker or FIGI must be defined!") 2653 raise Exception("Ticker or FIGI required") 2654 2655 if self.ticker and not self.figi: 2656 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2657 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2658 2659 if self.figi and not self.ticker: 2660 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2661 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2662 2663 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2664 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2665 if interval.lower() != "day": 2666 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2667 2668 delta = dtEnd - dtStart # current UTC time minus last time in file 2669 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2670 2671 # calculate history length in candles: 2672 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2673 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2674 length += 1 # to avoid fraction time 2675 2676 # calculate data blocks count: 2677 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2678 2679 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2680 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2681 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2682 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2683 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2684 2685 tempOld = None # pandas object for old history, if --only-missing key present 2686 lastTime = None # datetime object of last old candle in file 2687 2688 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2689 uLogger.debug("--only-missing key present, add only last missing candles...") 2690 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2691 2692 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2693 2694 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2695 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2696 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2697 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2698 2699 # get last datetime object from last string in file or minus 1 delta if file is empty: 2700 if len(tempOld) > 0: 2701 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2702 2703 else: 2704 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2705 2706 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2707 2708 responseJSONs = [] # raw history blocks of data 2709 2710 blockEnd = dtEnd 2711 for item in range(blocks): 2712 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2713 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2714 2715 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2716 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2717 )) 2718 2719 if blockStart == blockEnd: 2720 uLogger.debug("Skipped this zero-length block...") 2721 2722 else: 2723 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2724 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2725 self.body = str({ 2726 "figi": self.figi, 2727 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2728 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2729 "interval": TKS_CANDLE_INTERVALS[interval][0] 2730 }) 2731 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2732 2733 if "code" in responseJSON.keys(): 2734 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2735 2736 else: 2737 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2738 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2739 2740 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2741 2742 blockEnd = blockStart 2743 2744 printCount = len(responseJSONs) # candles to show in console 2745 if responseJSONs: 2746 tempHistory = pd.DataFrame( 2747 data={ 2748 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2749 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2750 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2751 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2752 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2753 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2754 "volume": [int(item["volume"]) for item in responseJSONs], 2755 }, 2756 index=range(len(responseJSONs)), 2757 columns=["date", "time", "open", "high", "low", "close", "volume"], 2758 ) 2759 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2760 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2761 2762 # append only newest candles to old history if --only-missing key present: 2763 if onlyMissing and tempOld is not None and lastTime is not None: 2764 index = 0 # find start index in tempHistory data: 2765 2766 for i, item in tempHistory.iterrows(): 2767 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2768 2769 if curTime == lastTime: 2770 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2771 index = i 2772 printCount = index + 1 2773 break 2774 2775 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2776 2777 else: 2778 history = tempHistory # if no `--only-missing` key then load full data from server 2779 2780 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2781 2782 if history is not None and not history.empty: 2783 if show: 2784 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2785 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2786 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2787 )) 2788 2789 else: 2790 uLogger.warning("Received an empty candles history!") 2791 2792 if self.historyFile is not None: 2793 if history is not None and not history.empty: 2794 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2795 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2796 2797 else: 2798 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2799 2800 else: 2801 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2802 2803 return history 2804 2805 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2806 """ 2807 Load candles history from csv-file and return Pandas DataFrame object. 2808 2809 See also: `History()` and `ShowHistoryChart()` methods. 2810 2811 :param filePath: path to csv-file to open. 2812 """ 2813 loadedHistory = None # init candles data object 2814 2815 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2816 2817 if os.path.exists(filePath): 2818 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2819 2820 tfStr = self.priceModel.FormattedDelta( 2821 self.priceModel.timeframe, 2822 "{days} days {hours}h {minutes}m {seconds}s", 2823 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2824 self.priceModel.timeframe, 2825 "{hours}h {minutes}m {seconds}s", 2826 ) 2827 2828 if loadedHistory is not None and not loadedHistory.empty: 2829 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2830 len(loadedHistory), 2831 tfStr, 2832 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2833 ) 2834 2835 else: 2836 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2837 2838 else: 2839 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2840 2841 return loadedHistory 2842 2843 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2844 """ 2845 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2846 2847 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2848 Default: `index.html` (both for interact and non-interact candlesticks chart). 2849 2850 See also: `History()` and `LoadHistory()` methods. 2851 2852 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2853 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2854 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2855 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2856 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2857 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2858 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2859 """ 2860 if isinstance(candles, str): 2861 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2862 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2863 2864 elif isinstance(candles, pd.DataFrame): 2865 self.priceModel.prices = candles # set candles chain from variable 2866 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2867 2868 if "datetime" not in candles.columns: 2869 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2870 2871 else: 2872 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2873 raise Exception("Incorrect value") 2874 2875 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2876 2877 if interact: 2878 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2879 2880 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2881 2882 else: 2883 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2884 2885 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2886 2887 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2888 2889 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2890 """ 2891 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2892 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2893 2894 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2895 2896 :param operation: string "Buy" or "Sell". 2897 :param lots: volume, integer count of lots >= 1. 2898 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2899 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2900 :param expDate: string "Undefined" by default or local date in future, 2901 it is a string with format `%Y-%m-%d %H:%M:%S`. 2902 :return: JSON with response from broker server. 2903 """ 2904 if self.accountId is None or not self.accountId: 2905 uLogger.error("Variable `accountId` must be defined for using this method!") 2906 raise Exception("Account ID required") 2907 2908 if operation is None or not operation or operation not in ("Buy", "Sell"): 2909 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2910 raise Exception("Incorrect value") 2911 2912 if lots is None or lots < 1: 2913 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2914 lots = 1 2915 2916 if tp is None or tp < 0: 2917 tp = 0 2918 2919 if sl is None or sl < 0: 2920 sl = 0 2921 2922 if expDate is None or not expDate: 2923 expDate = "Undefined" 2924 2925 if not (self.ticker or self.figi): 2926 uLogger.error("Ticker or FIGI must be defined!") 2927 raise Exception("Ticker or FIGI required") 2928 2929 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 2930 self.ticker = instrument["ticker"] 2931 self.figi = instrument["figi"] 2932 2933 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2934 2935 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2936 self.body = str({ 2937 "figi": self.figi, 2938 "quantity": str(lots), 2939 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2940 "accountId": str(self.accountId), 2941 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2942 }) 2943 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 2944 2945 if "orderId" in response.keys(): 2946 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2947 operation, response["orderId"], 2948 self.ticker, self.figi, lots, 2949 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2950 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2951 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2952 )) 2953 2954 if tp > 0: 2955 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2956 2957 if sl > 0: 2958 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2959 2960 else: 2961 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.") 2962 2963 return response 2964 2965 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2966 """ 2967 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2968 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2969 2970 See also: `Order()` and `Trade()` docstrings. 2971 2972 :param lots: volume, integer count of lots >= 1. 2973 :param tp: float > 0, take profit price of stop-order. 2974 :param sl: float > 0, stop loss price of stop-order. 2975 :param expDate: it's a local date in future. 2976 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2977 :return: JSON with response from broker server. 2978 """ 2979 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 2980 2981 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2982 """ 2983 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2984 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2985 2986 See also: `Order()` and `Trade()` docstrings. 2987 2988 :param lots: volume, integer count of lots >= 1. 2989 :param tp: float > 0, take profit price of stop-order. 2990 :param sl: float > 0, stop loss price of stop-order. 2991 :param expDate: it's a local date in the future. 2992 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2993 :return: JSON with response from broker server. 2994 """ 2995 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 2996 2997 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 2998 """ 2999 Close position of given instruments. 3000 3001 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3002 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3003 This avoids unnecessary downloading data from the server. 3004 """ 3005 if instruments is None or not instruments: 3006 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3007 raise Exception("Ticker or FIGI required") 3008 3009 if isinstance(instruments, str): 3010 instruments = [instruments] 3011 3012 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3013 if uniqueInstruments: 3014 if portfolio is None or not portfolio: 3015 portfolio = self.Overview(show=False) 3016 3017 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3018 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3019 3020 for self.figi in uniqueInstruments: 3021 if self.figi not in allOpened: 3022 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi)) 3023 continue 3024 3025 # search open trade info about instrument by ticker: 3026 instrument = {} 3027 for iType in TKS_INSTRUMENTS: 3028 if instrument: 3029 break 3030 3031 for item in portfolio["stat"][iType]: 3032 if item["figi"] == self.figi: 3033 instrument = item 3034 break 3035 3036 if instrument: 3037 self.ticker = instrument["ticker"] 3038 self.figi = instrument["figi"] 3039 3040 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3041 self.ticker, 3042 self.figi, 3043 int(instrument["volume"]), 3044 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3045 )) 3046 3047 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3048 3049 if tradeLots > 0: 3050 if instrument["blocked"] > 0: 3051 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3052 instrument["blocked"], 3053 self.ticker, 3054 tradeLots, 3055 )) 3056 3057 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3058 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3059 3060 else: 3061 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker)) 3062 3063 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3064 """ 3065 Close all positions of given instruments with defined type. 3066 3067 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3068 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3069 This avoids unnecessary downloading data from the server. 3070 """ 3071 if iType not in TKS_INSTRUMENTS: 3072 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3073 3074 else: 3075 if portfolio is None or not portfolio: 3076 portfolio = self.Overview(show=False) 3077 3078 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3079 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3080 3081 if tickers and portfolio: 3082 self.CloseTrades(tickers, portfolio) 3083 3084 else: 3085 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3086 3087 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3088 """ 3089 Universal method to create market or limit orders with all available parameters for current `accountId`. 3090 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3091 3092 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3093 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3094 3095 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3096 then broker immediately open market order as you can do simple --buy or --sell operations! 3097 3098 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3099 When current price will go up or down to target price value then broker opens a limit order. 3100 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3101 3102 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3103 3104 :param operation: string "Buy" or "Sell". 3105 :param orderType: string "Limit" or "Stop". 3106 :param lots: volume, integer count of lots >= 1. 3107 :param targetPrice: target price > 0. This is open trade price for limit order. 3108 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3109 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3110 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3111 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3112 Stop loss order always executed by market price. 3113 :param expDate: string "Undefined" by default or local date in future. 3114 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3115 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3116 A limit order has no expiration date, it lasts until the end of the trading day. 3117 :return: JSON with response from broker server. 3118 """ 3119 if self.accountId is None or not self.accountId: 3120 uLogger.error("Variable `accountId` must be defined for using this method!") 3121 raise Exception("Account ID required") 3122 3123 if operation is None or not operation or operation not in ("Buy", "Sell"): 3124 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3125 raise Exception("Incorrect value") 3126 3127 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3128 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3129 raise Exception("Incorrect value") 3130 3131 if lots is None or lots < 1: 3132 uLogger.error("You must define trade volume > 0: integer count of lots!") 3133 raise Exception("Incorrect value") 3134 3135 if targetPrice is None or targetPrice <= 0: 3136 uLogger.error("Target price for limit-order must be greater than 0!") 3137 raise Exception("Incorrect value") 3138 3139 if limitPrice is None or limitPrice <= 0: 3140 limitPrice = targetPrice 3141 3142 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3143 stopType = "Limit" 3144 3145 if expDate is None or not expDate: 3146 expDate = "Undefined" 3147 3148 if not (self.ticker or self.figi): 3149 uLogger.error("Tocker or FIGI must be defined!") 3150 raise Exception("Ticker or FIGI required") 3151 3152 response = {} 3153 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 3154 self.ticker = instrument["ticker"] 3155 self.figi = instrument["figi"] 3156 3157 if orderType == "Limit": 3158 uLogger.debug( 3159 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3160 self.ticker, self.figi, 3161 operation, lots, targetPrice, instrument["currency"], 3162 )) 3163 3164 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3165 self.body = str({ 3166 "figi": self.figi, 3167 "quantity": str(lots), 3168 "price": FloatToNano(targetPrice), 3169 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3170 "accountId": str(self.accountId), 3171 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3172 }) 3173 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3174 3175 if "orderId" in response.keys(): 3176 uLogger.info( 3177 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3178 response["orderId"], 3179 self.ticker, self.figi, 3180 operation, lots, targetPrice, instrument["currency"], 3181 )) 3182 3183 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3184 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3185 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3186 targetPrice, instrument["currency"], 3187 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3188 )) 3189 3190 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3191 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3192 targetPrice, instrument["currency"], 3193 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3194 )) 3195 3196 else: 3197 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3198 3199 if orderType == "Stop": 3200 uLogger.debug( 3201 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3202 self.ticker, self.figi, 3203 operation, lots, 3204 targetPrice, instrument["currency"], 3205 limitPrice, instrument["currency"], 3206 stopType, expDate, 3207 )) 3208 3209 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3210 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3211 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3212 3213 body = { 3214 "figi": self.figi, 3215 "quantity": str(lots), 3216 "price": FloatToNano(limitPrice), 3217 "stopPrice": FloatToNano(targetPrice), 3218 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3219 "accountId": str(self.accountId), 3220 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3221 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3222 } 3223 3224 if expDateUTC: 3225 body["expireDate"] = expDateUTC 3226 3227 self.body = str(body) 3228 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3229 3230 if "stopOrderId" in response.keys(): 3231 uLogger.info( 3232 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3233 response["stopOrderId"], 3234 self.ticker, self.figi, 3235 operation, lots, 3236 targetPrice, instrument["currency"], 3237 limitPrice, instrument["currency"], 3238 TKS_STOP_ORDER_TYPES[stopOrderType], 3239 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3240 )) 3241 3242 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3243 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3244 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3245 targetPrice, instrument["currency"], 3246 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3247 )) 3248 3249 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3250 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3251 targetPrice, instrument["currency"], 3252 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3253 )) 3254 3255 else: 3256 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3257 3258 return response 3259 3260 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3261 """ 3262 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3263 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3264 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3265 See also: `Order()` docstring. 3266 3267 :param lots: volume, integer count of lots >= 1. 3268 :param targetPrice: target price > 0. This is open trade price for limit order. 3269 :return: JSON with response from broker server. 3270 """ 3271 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3272 3273 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3274 """ 3275 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3276 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3277 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3278 target price value then broker opens a limit order. See also: `Order()` docstring. 3279 3280 :param lots: volume, integer count of lots >= 1. 3281 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3282 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3283 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3284 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3285 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3286 :param expDate: string "Undefined" by default or local date in future. 3287 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3288 This date is converting to UTC format for server. 3289 :return: JSON with response from broker server. 3290 """ 3291 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3292 3293 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3294 """ 3295 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3296 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3297 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3298 See also: `Order()` docstring. 3299 3300 :param lots: volume, integer count of lots >= 1. 3301 :param targetPrice: target price > 0. This is open trade price for limit order. 3302 :return: JSON with response from broker server. 3303 """ 3304 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3305 3306 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3307 """ 3308 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3309 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3310 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3311 target price value then broker opens a limit order. See also: `Order()` docstring. 3312 3313 :param lots: volume, integer count of lots >= 1. 3314 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3315 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3316 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3317 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3318 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3319 :param expDate: string "Undefined" by default or local date in future. 3320 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3321 This date is converting to UTC format for server. 3322 :return: JSON with response from broker server. 3323 """ 3324 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3325 3326 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3327 """ 3328 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3329 3330 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3331 :param allOrdersIDs: pre-received lists of all active pending orders. 3332 This avoids unnecessary downloading data from the server. 3333 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3334 """ 3335 if self.accountId is None or not self.accountId: 3336 uLogger.error("Variable `accountId` must be defined for using this method!") 3337 raise Exception("Account ID required") 3338 3339 if orderIDs: 3340 if allOrdersIDs is None or not allOrdersIDs: 3341 rawOrders = self.RequestPendingOrders() 3342 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3343 3344 if allStopOrdersIDs is None or not allStopOrdersIDs: 3345 rawStopOrders = self.RequestStopOrders() 3346 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3347 3348 for orderID in orderIDs: 3349 idInPendingOrders = orderID in allOrdersIDs 3350 idInStopOrders = orderID in allStopOrdersIDs 3351 3352 if not (idInPendingOrders or idInStopOrders): 3353 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3354 continue 3355 3356 else: 3357 if idInPendingOrders: 3358 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3359 3360 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3361 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3362 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3363 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3364 3365 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3366 if self.moreDebug: 3367 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3368 3369 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3370 3371 else: 3372 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3373 3374 elif idInStopOrders: 3375 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3376 3377 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3378 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3379 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3380 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3381 3382 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3383 if self.moreDebug: 3384 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3385 3386 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3387 3388 else: 3389 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3390 3391 else: 3392 continue 3393 3394 def CloseAllOrders(self) -> None: 3395 """ 3396 Gets a list of open pending and stop orders and cancel it all. 3397 """ 3398 rawOrders = self.RequestPendingOrders() 3399 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3400 lenOrders = len(allOrdersIDs) 3401 3402 rawStopOrders = self.RequestStopOrders() 3403 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3404 lenSOrders = len(allStopOrdersIDs) 3405 3406 if lenOrders > 0 or lenSOrders > 0: 3407 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3408 3409 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3410 3411 else: 3412 uLogger.info("Orders not found, nothing to cancel.") 3413 3414 def CloseAll(self, *args) -> None: 3415 """ 3416 Close all available (not blocked) opened trades and orders. 3417 3418 Also, you can select one or more keywords case-insensitive: 3419 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3420 3421 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3422 """ 3423 overview = self.Overview(show=False) # get all open trades info 3424 3425 if len(args) == 0: 3426 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3427 self.CloseAllOrders() # close all pending and stop orders 3428 3429 for iType in TKS_INSTRUMENTS: 3430 if iType != "Currencies": 3431 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3432 3433 else: 3434 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3435 lowerArgs = [x.lower() for x in args] 3436 3437 if "orders" in lowerArgs: 3438 self.CloseAllOrders() # close all pending and stop orders 3439 3440 for iType in TKS_INSTRUMENTS: 3441 if iType.lower() in lowerArgs and iType != "Currencies": 3442 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3443 3444 @staticmethod 3445 def ParseOrderParameters(operation, **inputParameters): 3446 """ 3447 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3448 3449 :param operation: string "Buy" or "Sell". 3450 :param inputParameters: this is dict of strings that looks like this 3451 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3452 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3453 "prices" key: one or more prices to open limit-orders 3454 Counts of values in lots and prices lists must be equals! 3455 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3456 """ 3457 # TODO: update order grid work with api v2 3458 pass 3459 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3460 # 3461 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3462 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3463 # raise Exception("Incorrect value") 3464 # 3465 # if "l" in inputParameters.keys(): 3466 # inputParameters["lots"] = inputParameters.pop("l") 3467 # 3468 # if "p" in inputParameters.keys(): 3469 # inputParameters["prices"] = inputParameters.pop("p") 3470 # 3471 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3472 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3473 # raise Exception("Incorrect value") 3474 # 3475 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3476 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3477 # 3478 # if len(lots) != len(prices): 3479 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3480 # raise Exception("Incorrect value") 3481 # 3482 # uLogger.debug("Extracted parameters for orders:") 3483 # uLogger.debug("lots = {}".format(lots)) 3484 # uLogger.debug("prices = {}".format(prices)) 3485 # 3486 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3487 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3488 # uLogger.debug("Order parameters: {}".format(result)) 3489 # 3490 # return result 3491 3492 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3493 """ 3494 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3495 3496 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3497 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3498 """ 3499 result = False 3500 msg = "Instrument not defined!" 3501 3502 if portfolio is None or not portfolio: 3503 portfolio = self.Overview(show=False) 3504 3505 if self.ticker: 3506 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3507 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3508 3509 for iType in TKS_INSTRUMENTS: 3510 for instrument in portfolio["stat"][iType]: 3511 if instrument["ticker"] == self.ticker: 3512 result = True 3513 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3514 break 3515 3516 elif self.figi: 3517 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3518 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3519 3520 for iType in TKS_INSTRUMENTS: 3521 for instrument in portfolio["stat"][iType]: 3522 if instrument["figi"] == self.figi: 3523 result = True 3524 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3525 break 3526 3527 else: 3528 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3529 3530 uLogger.debug(msg) 3531 3532 return result 3533 3534 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3535 """ 3536 Returns instrument from the user's portfolio if it presents there. 3537 Instrument must be defined by `ticker` (highly priority) or `figi`. 3538 3539 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3540 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3541 """ 3542 result = None 3543 msg = "Instrument not defined!" 3544 3545 if portfolio is None or not portfolio: 3546 portfolio = self.Overview(show=False) 3547 3548 if self.ticker: 3549 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self.ticker)) 3550 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3551 3552 for iType in TKS_INSTRUMENTS: 3553 for instrument in portfolio["stat"][iType]: 3554 if instrument["ticker"] == self.ticker: 3555 result = instrument 3556 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3557 break 3558 3559 elif self.figi: 3560 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3561 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3562 3563 for iType in TKS_INSTRUMENTS: 3564 for instrument in portfolio["stat"][iType]: 3565 if instrument["figi"] == self.figi: 3566 result = instrument 3567 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3568 break 3569 3570 else: 3571 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3572 3573 uLogger.debug(msg) 3574 3575 return result 3576 3577 def RequestLimits(self) -> dict: 3578 """ 3579 Method for obtaining the available funds for withdrawal for current `accountId`. 3580 3581 See also: 3582 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3583 - `OverviewLimits()` method 3584 3585 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3586 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3587 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3588 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3589 """ 3590 if self.accountId is None or not self.accountId: 3591 uLogger.error("Variable `accountId` must be defined for using this method!") 3592 raise Exception("Account ID required") 3593 3594 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3595 3596 self.body = str({"accountId": self.accountId}) 3597 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3598 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3599 3600 if self.moreDebug: 3601 uLogger.debug("Records about available funds for withdrawal successfully received") 3602 3603 return rawLimits 3604 3605 def OverviewLimits(self, show: bool = False) -> dict: 3606 """ 3607 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3608 3609 See also: `RequestLimits()`. 3610 3611 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3612 :return: dict with raw parsed data from server and some calculated statistics about it. 3613 """ 3614 if self.accountId is None or not self.accountId: 3615 uLogger.error("Variable `accountId` must be defined for using this method!") 3616 raise Exception("Account ID required") 3617 3618 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3619 3620 view = { 3621 "rawLimits": rawLimits, 3622 "limits": { # parsed data for every currency: 3623 "money": { # this is an array of portfolio currency positions 3624 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3625 }, 3626 "blocked": { # this is an array of blocked currency 3627 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3628 }, 3629 "blockedGuarantee": { # this is locked money under collateral for futures 3630 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3631 }, 3632 }, 3633 } 3634 3635 # --- Prepare text table with limits in human-readable format: 3636 if show: 3637 info = [ 3638 "# Withdrawal limits\n\n", 3639 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3640 "* **Account ID:** [{}]\n".format(self.accountId), 3641 ] 3642 3643 if view["limits"]["money"]: 3644 info.extend([ 3645 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3646 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3647 ]) 3648 3649 else: 3650 info.append("\nNo withdrawal limits\n") 3651 3652 for curr in view["limits"]["money"].keys(): 3653 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3654 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3655 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3656 3657 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3658 "[{}]".format(curr), 3659 "{:.2f}".format(view["limits"]["money"][curr]), 3660 "{:.2f}".format(availableMoney), 3661 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3662 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3663 ) 3664 3665 if curr == "rub": 3666 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3667 3668 else: 3669 info.append(infoStr) 3670 3671 infoText = "".join(info) 3672 3673 uLogger.info(infoText) 3674 3675 if self.withdrawalLimitsFile: 3676 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3677 fH.write(infoText) 3678 3679 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3680 3681 return view 3682 3683 def RequestAccounts(self) -> dict: 3684 """ 3685 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3686 3687 See also: 3688 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3689 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3690 - `OverviewUserInfo()` method 3691 3692 :return: dict with raw data from server that contains accounts info. Example of dict: 3693 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3694 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3695 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3696 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3697 """ 3698 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3699 3700 self.body = str({}) 3701 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3702 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3703 3704 if self.moreDebug: 3705 uLogger.debug("Records about available accounts successfully received") 3706 3707 return rawAccounts 3708 3709 def RequestUserInfo(self) -> dict: 3710 """ 3711 Method for requesting common user's information. 3712 3713 See also: 3714 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3715 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3716 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3717 - `OverviewUserInfo()` method 3718 3719 :return: dict with raw data from server that contains user's information. Example of dict: 3720 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3721 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3722 """ 3723 uLogger.debug("Requesting common user's information. Wait, please...") 3724 3725 self.body = str({}) 3726 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3727 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3728 3729 if self.moreDebug: 3730 uLogger.debug("Records about current user successfully received") 3731 3732 return rawUserInfo 3733 3734 def RequestMarginStatus(self, accountId: str = None) -> dict: 3735 """ 3736 Method for requesting margin calculation for defined account ID. 3737 3738 See also: 3739 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3740 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3741 - `OverviewUserInfo()` method 3742 3743 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3744 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3745 Example of responses: 3746 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3747 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3748 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3749 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3750 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3751 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3752 """ 3753 if accountId is None or not accountId: 3754 if self.accountId is None or not self.accountId: 3755 uLogger.error("Variable `accountId` must be defined for using this method!") 3756 raise Exception("Account ID required") 3757 3758 else: 3759 accountId = self.accountId # use `self.accountId` (main ID) by default 3760 3761 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3762 3763 self.body = str({"accountId": accountId}) 3764 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3765 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3766 3767 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3768 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3769 rawMargin = {} 3770 3771 else: 3772 if self.moreDebug: 3773 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3774 3775 return rawMargin 3776 3777 def RequestTariffLimits(self) -> dict: 3778 """ 3779 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3780 3781 See also: 3782 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3783 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3784 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3785 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3786 - `OverviewUserInfo()` method 3787 3788 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3789 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3790 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3791 """ 3792 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3793 3794 self.body = str({}) 3795 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3796 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3797 3798 if self.moreDebug: 3799 uLogger.debug("Records with limits of current tariff successfully received") 3800 3801 return rawTariffLimits 3802 3803 def RequestBondCoupons(self, iJSON: dict) -> dict: 3804 """ 3805 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3806 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3807 All dates are in UTC timezone. 3808 3809 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3810 Documentation: 3811 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3812 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3813 3814 See also: `ExtendBondsData()`. 3815 3816 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3817 If raw iJSON is not data of bond then server returns an error [400] with message: 3818 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3819 :return: dictionary with bond payment calendar. Response example 3820 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3821 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3822 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3823 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3824 """ 3825 if iJSON["figi"] is None or not iJSON["figi"]: 3826 uLogger.error("FIGI must be defined for using this method!") 3827 raise Exception("FIGI required") 3828 3829 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3830 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3831 3832 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3833 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3834 self.figi, 3835 startDate, 3836 endDate, 3837 )) 3838 3839 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3840 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3841 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 3842 3843 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3844 uLogger.warning("Instrument type is not bond!") 3845 3846 else: 3847 if self.moreDebug: 3848 uLogger.debug("Records about bond payment calendar successfully received") 3849 3850 return calendar 3851 3852 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3853 """ 3854 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3855 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 3856 coupon yields, current yields and some statistics etc. 3857 3858 WARNING! This is too long operation if a lot of bonds requested from broker server. 3859 3860 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3861 3862 :param instruments: list of strings with tickers or FIGIs. 3863 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3864 for further used by data scientists or stock analytics. 3865 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 3866 In XLSX-file and Pandas DataFrame fields mean: 3867 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3868 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3869 """ 3870 if instruments is None or not instruments: 3871 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3872 raise Exception("Ticker or FIGI required") 3873 3874 if isinstance(instruments, str): 3875 instruments = [instruments] 3876 3877 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3878 3879 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3880 3881 iCount = len(uniqueInstruments) 3882 tooLong = iCount >= 20 3883 if tooLong: 3884 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3885 3886 bonds = None 3887 for i, self.figi in enumerate(uniqueInstruments): 3888 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3889 3890 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3891 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3892 rawBond = self.SearchByFIGI(requestPrice=True) 3893 3894 # Widen raw data with UTC current time (iData["actualDateTime"]): 3895 actualDate = datetime.now(tzutc()) 3896 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3897 3898 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3899 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3900 3901 # Replace some values with human-readable: 3902 iData["nominalCurrency"] = iData["nominal"]["currency"] 3903 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3904 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3905 iData["aciCurrency"] = iData["aciValue"]["currency"] 3906 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3907 iData["issueSize"] = int(iData["issueSize"]) 3908 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3909 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3910 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3911 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3912 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3913 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3914 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3915 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3916 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3917 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3918 3919 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3920 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3921 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3922 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3923 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3924 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3925 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3926 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3927 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3928 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3929 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3930 3931 # Widen raw data with calendar data from `rawCalendar` values: 3932 calendarData = [] 3933 if "events" in iData["rawCalendar"].keys(): 3934 for item in iData["rawCalendar"]["events"]: 3935 calendarData.append({ 3936 "couponDate": item["couponDate"], 3937 "couponNumber": int(item["couponNumber"]), 3938 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3939 "payCurrency": item["payOneBond"]["currency"], 3940 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3941 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3942 "couponStartDate": item["couponStartDate"], 3943 "couponEndDate": item["couponEndDate"], 3944 "couponPeriod": item["couponPeriod"], 3945 }) 3946 3947 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3948 if "maturityDate" not in iData.keys(): 3949 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3950 3951 # Widen raw data with Coupon Rate. 3952 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3953 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3954 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3955 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3956 3957 # Widen raw data with Yield to Maturity (YTM) on current date. 3958 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3959 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3960 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3961 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3962 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3963 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3964 3965 iData["calendar"] = calendarData # adds calendar at the end 3966 3967 # Remove not used data: 3968 iData.pop("uid") 3969 iData.pop("positionUid") 3970 iData.pop("currentPrice") 3971 iData.pop("rawCalendar") 3972 3973 colNames = list(iData.keys()) 3974 if bonds is None: 3975 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3976 3977 else: 3978 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 3979 3980 else: 3981 uLogger.warning("Instrument is not a bond!") 3982 3983 processed = round(100 * (i + 1) / iCount, 1) 3984 if tooLong and processed % 5 == 0: 3985 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 3986 3987 else: 3988 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 3989 3990 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 3991 3992 # Saving bonds from Pandas DataFrame to XLSX sheet: 3993 if xlsx and self.bondsXLSXFile: 3994 with pd.ExcelWriter( 3995 path=self.bondsXLSXFile, 3996 date_format=TKS_DATE_FORMAT, 3997 datetime_format=TKS_DATE_TIME_FORMAT, 3998 mode="w", 3999 ) as writer: 4000 bonds.to_excel( 4001 writer, 4002 sheet_name="Extended bonds data", 4003 index=True, 4004 encoding="UTF-8", 4005 freeze_panes=(1, 1), 4006 ) # saving as XLSX-file with freeze first row and column as headers 4007 4008 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4009 4010 return bonds 4011 4012 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4013 """ 4014 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4015 4016 WARNING! This is too long operation if a lot of bonds requested from broker server. 4017 4018 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4019 4020 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4021 extended information about bonds: main info, current prices, bond payment calendar, 4022 coupon yields, current yields and some statistics etc. 4023 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4024 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4025 for further used by data scientists or stock analytics. 4026 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4027 """ 4028 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4029 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4030 4031 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4032 4033 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4034 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4035 calendar = None 4036 for bond in extBonds.iterrows(): 4037 for item in bond[1]["calendar"]: 4038 cData = { 4039 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4040 "couponDate": item["couponDate"], 4041 "figi": bond[1]["figi"], 4042 "ticker": bond[1]["ticker"], 4043 "name": bond[1]["name"], 4044 "couponNumber": item["couponNumber"], 4045 "payOneBond": item["payOneBond"], 4046 "payCurrency": item["payCurrency"], 4047 "couponType": item["couponType"], 4048 "couponPeriod": item["couponPeriod"], 4049 "fixDate": item["fixDate"], 4050 "couponStartDate": item["couponStartDate"], 4051 "couponEndDate": item["couponEndDate"], 4052 } 4053 4054 if calendar is None: 4055 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4056 4057 else: 4058 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4059 4060 if calendar is not None: 4061 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4062 4063 # Saving calendar from Pandas DataFrame to XLSX sheet: 4064 if xlsx: 4065 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4066 4067 with pd.ExcelWriter( 4068 path=xlsxCalendarFile, 4069 date_format=TKS_DATE_FORMAT, 4070 datetime_format=TKS_DATE_TIME_FORMAT, 4071 mode="w", 4072 ) as writer: 4073 humanReadable = calendar.copy(deep=True) 4074 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4075 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4076 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4077 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4078 humanReadable.columns = colNames # human-readable column names 4079 4080 humanReadable.to_excel( 4081 writer, 4082 sheet_name="Bond payments calendar", 4083 index=False, 4084 encoding="UTF-8", 4085 freeze_panes=(1, 2), 4086 ) # saving as XLSX-file with freeze first row and column as headers 4087 4088 del humanReadable # release df in memory 4089 4090 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4091 4092 return calendar 4093 4094 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4095 """ 4096 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4097 Also, creates Markdown file with calendar data, `calendar.md` by default. 4098 4099 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4100 4101 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4102 extended information about bonds: main info, current prices, bond payment calendar, 4103 coupon yields, current yields and some statistics etc. 4104 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4105 :param show: if `True` then also printing bonds payment calendar to the console, 4106 otherwise save to file `calendarFile` only. `False` by default. 4107 :return: multilines text in Markdown format with bonds payment calendar as a table. 4108 """ 4109 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4110 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4111 4112 infoText = "# Bond payments calendar\n\n" 4113 4114 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4115 4116 if not (calendar is None or calendar.empty): 4117 splitLine = "| | | | | | | | | |\n" 4118 4119 info = [ 4120 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4121 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4122 ] 4123 4124 newMonth = False 4125 notOneBond = calendar["figi"].nunique() > 1 4126 for i, bond in enumerate(calendar.iterrows()): 4127 if newMonth and notOneBond: 4128 info.append(splitLine) 4129 4130 info.append( 4131 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4132 " √" if bond[1]["paid"] else " —", 4133 bond[1]["couponDate"].split("T")[0], 4134 bond[1]["figi"], 4135 bond[1]["ticker"], 4136 bond[1]["couponNumber"], 4137 "{} {}".format( 4138 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4139 bond[1]["payCurrency"], 4140 ), 4141 bond[1]["couponType"], 4142 bond[1]["couponPeriod"], 4143 bond[1]["fixDate"].split("T")[0], 4144 ) 4145 ) 4146 4147 if i < len(calendar.values) - 1: 4148 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4149 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4150 newMonth = False if curDate.month == nextDate.month else True 4151 4152 else: 4153 newMonth = False 4154 4155 infoText += "".join(info) 4156 4157 if show: 4158 uLogger.info("{}".format(infoText)) 4159 4160 if self.calendarFile is not None: 4161 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4162 fH.write(infoText) 4163 4164 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4165 4166 else: 4167 infoText += "No data\n" 4168 4169 return infoText 4170 4171 def OverviewAccounts(self, show: bool = False) -> dict: 4172 """ 4173 Method for parsing and show simple table with all available user accounts. 4174 4175 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4176 4177 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4178 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4179 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4180 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4181 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4182 "closed": "—", "access": "Full access" }, ...}}` 4183 """ 4184 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4185 4186 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4187 accounts = { 4188 item["id"]: { 4189 "type": TKS_ACCOUNT_TYPES[item["type"]], 4190 "name": item["name"], 4191 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4192 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4193 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4194 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4195 } for item in rawAccounts["accounts"] 4196 } 4197 4198 # Raw and parsed data with some fields replaced in "stat" section: 4199 view = { 4200 "rawAccounts": rawAccounts, 4201 "stat": accounts, 4202 } 4203 4204 # --- Prepare simple text table with only accounts data in human-readable format: 4205 if show: 4206 info = [ 4207 "# User accounts\n\n", 4208 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4209 "| Account ID | Type | Status | Name |\n", 4210 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4211 ] 4212 4213 for account in view["stat"].keys(): 4214 info.extend([ 4215 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4216 account, 4217 view["stat"][account]["type"], 4218 view["stat"][account]["status"], 4219 view["stat"][account]["name"], 4220 ) 4221 ]) 4222 4223 infoText = "".join(info) 4224 4225 uLogger.info(infoText) 4226 4227 if self.userAccountsFile: 4228 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4229 fH.write(infoText) 4230 4231 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4232 4233 return view 4234 4235 def OverviewUserInfo(self, show: bool = False) -> dict: 4236 """ 4237 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4238 4239 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4240 4241 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4242 :return: dict with raw parsed data from server and some calculated statistics about it. 4243 """ 4244 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4245 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4246 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4247 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4248 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4249 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4250 4251 # This is dict with parsed common user data: 4252 userInfo = { 4253 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4254 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4255 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4256 "tariff": rawUserInfo["tariff"], 4257 } 4258 4259 # This is an array of dict with parsed margin statuses for every account IDs: 4260 margins = {} 4261 for accountId in accounts.keys(): 4262 if rawMargins[accountId]: 4263 margins[accountId] = { 4264 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4265 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4266 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4267 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4268 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4269 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4270 } 4271 4272 else: 4273 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4274 4275 unary = {} # unary-connection limits 4276 for item in rawTariffLimits["unaryLimits"]: 4277 if item["limitPerMinute"] in unary.keys(): 4278 unary[item["limitPerMinute"]].extend(item["methods"]) 4279 4280 else: 4281 unary[item["limitPerMinute"]] = item["methods"] 4282 4283 stream = {} # stream-connection limits 4284 for item in rawTariffLimits["streamLimits"]: 4285 if item["limit"] in stream.keys(): 4286 stream[item["limit"]].extend(item["streams"]) 4287 4288 else: 4289 stream[item["limit"]] = item["streams"] 4290 4291 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4292 limits = { 4293 "unary": unary, 4294 "stream": stream, 4295 } 4296 4297 # Raw and parsed data as an output result: 4298 view = { 4299 "rawUserInfo": rawUserInfo, 4300 "rawAccounts": rawAccounts, 4301 "rawMargins": rawMargins, 4302 "rawTariffLimits": rawTariffLimits, 4303 "stat": { 4304 "userInfo": userInfo, 4305 "accounts": accounts, 4306 "margins": margins, 4307 "limits": limits, 4308 }, 4309 } 4310 4311 # --- Prepare text table with user information in human-readable format: 4312 if show: 4313 info = [ 4314 "# Full user information\n\n", 4315 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4316 "## Common information\n\n", 4317 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4318 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4319 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4320 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4321 "\n## User accounts\n\n", 4322 ] 4323 4324 for account in view["stat"]["accounts"].keys(): 4325 info.extend([ 4326 "### ID: [{}]\n\n".format(account), 4327 "| Parameters | Values |\n", 4328 "|----------------------|--------------------------------------------------------------|\n", 4329 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4330 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4331 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4332 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4333 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4334 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4335 ]) 4336 4337 if margins[account]: 4338 info.extend([ 4339 "| Margin status: | Enabled |\n", 4340 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4341 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4342 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4343 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4344 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4345 ]) 4346 4347 else: 4348 info.append("| Margin status: | Disabled |\n\n") 4349 4350 info.extend([ 4351 "\n## Current user tariff limits\n", 4352 "\nSee also:\n", 4353 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4354 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4355 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4356 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4357 "\n### Unary limits\n", 4358 ]) 4359 4360 if unary: 4361 for key, values in sorted(unary.items()): 4362 info.append("\n* Max requests per minute: {}\n".format(key)) 4363 4364 for value in values: 4365 info.append(" - {}\n".format(value)) 4366 4367 else: 4368 info.append("\nNot available\n") 4369 4370 info.append("\n### Stream limits\n") 4371 4372 if stream: 4373 for key, values in sorted(stream.items()): 4374 info.append("\n* Max stream connections: {}\n".format(key)) 4375 4376 for value in values: 4377 info.append(" - {}\n".format(value)) 4378 4379 else: 4380 info.append("\nNot available\n") 4381 4382 infoText = "".join(info) 4383 4384 uLogger.info(infoText) 4385 4386 if self.userInfoFile: 4387 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4388 fH.write(infoText) 4389 4390 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4391 4392 return view
This class implements methods to work with Tinkoff broker server.
Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
About token: https://tinkoff.github.io/investAPI/token/
153 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 154 """ 155 Main class init. 156 157 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 158 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 159 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 160 :param useCache: use default cache file with raw data to use instead of `iList`. 161 True by default. Cache is auto-update if new day has come. 162 If you don't want to use cache and always updates raw data then set `useCache=False`. 163 :param defaultCache: path to default cache file. `dump.json` by default. 164 """ 165 if token is None or not token: 166 try: 167 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 168 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 169 170 except KeyError: 171 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 172 raise Exception("Token required") 173 174 else: 175 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 176 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 177 178 if accountId is None or not accountId: 179 try: 180 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 181 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 182 183 except KeyError: 184 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 185 186 else: 187 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 188 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 189 190 self.version = __version__ # duplicate here used TKSBrokerAPI main version 191 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 192 193 Latest version: https://pypi.org/project/tksbrokerapi/ 194 """ 195 196 self.aliases = TKS_TICKER_ALIASES 197 """Some aliases instead official tickers. 198 199 See also: `TKSEnums.TKS_TICKER_ALIASES` 200 """ 201 202 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 203 204 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 205 206 self.ticker = "" 207 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 208 209 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 210 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 211 212 See also: `SearchByTicker()`, `SearchInstruments()`. 213 """ 214 215 self.figi = "" 216 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 217 218 See also: `SearchByFIGI()`, `SearchInstruments()`. 219 """ 220 221 self.depth = 1 222 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 223 224 See also: `GetCurrentPrices()`. 225 """ 226 227 self.server = r"https://invest-public-api.tinkoff.ru/rest" 228 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 229 230 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 231 """ 232 233 uLogger.debug("Broker API server: {}".format(self.server)) 234 235 self.timeout = 15 236 """Server operations timeout in seconds. Default: `15`. 237 238 See also: `SendAPIRequest()`. 239 """ 240 241 self.headers = { 242 "Content-Type": "application/json", 243 "accept": "application/json", 244 "Authorization": "Bearer {}".format(self.token), 245 "x-app-name": "Tim55667757.TKSBrokerAPI", 246 } 247 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 248 249 See also: `SendAPIRequest()`. 250 """ 251 252 self.body = None 253 """Request body which send to broker server. Default: `None`. 254 255 See also: `SendAPIRequest()`. 256 """ 257 258 self.moreDebug = False 259 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 260 261 self.historyFile = None 262 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 263 264 See also: `History()`. 265 """ 266 267 self.htmlHistoryFile = "index.html" 268 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 269 270 See also: `ShowHistoryChart()`. 271 """ 272 273 self.instrumentsFile = "instruments.md" 274 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 275 276 See also: `ShowInstrumentsInfo()`. 277 """ 278 279 self.searchResultsFile = "search-results.md" 280 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 281 282 See also: `SearchInstruments()`. 283 """ 284 285 self.pricesFile = "prices.md" 286 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 287 288 See also: `GetListOfPrices()`. 289 """ 290 291 self.infoFile = "info.md" 292 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 293 294 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 295 """ 296 297 self.bondsXLSXFile = "ext-bonds.xlsx" 298 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 299 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 300 301 See also: `ExtendBondsData()`. 302 """ 303 304 self.calendarFile = "calendar.md" 305 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 306 307 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 308 309 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 310 """ 311 312 self.overviewFile = "overview.md" 313 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 314 315 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 316 """ 317 318 self.overviewDigestFile = "overview-digest.md" 319 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 320 321 See also: `Overview()` with parameter `details="digest"`. 322 """ 323 324 self.overviewPositionsFile = "overview-positions.md" 325 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 326 327 See also: `Overview()` with parameter `details="positions"`. 328 """ 329 330 self.overviewOrdersFile = "overview-orders.md" 331 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 332 333 See also: `Overview()` with parameter `details="orders"`. 334 """ 335 336 self.overviewAnalyticsFile = "overview-analytics.md" 337 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 338 339 See also: `Overview()` with parameter `details="analytics"`. 340 """ 341 342 self.overviewBondsCalendarFile = "overview-calendar.md" 343 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 344 345 See also: `Overview()` with parameter `details="calendar"`. 346 """ 347 348 self.reportFile = "deals.md" 349 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 350 351 See also: `Deals()`. 352 """ 353 354 self.withdrawalLimitsFile = "limits.md" 355 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 356 357 See also: `OverviewLimits()` and `RequestLimits()`. 358 """ 359 360 self.userInfoFile = "user-info.md" 361 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 362 363 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 364 """ 365 366 self.userAccountsFile = "accounts.md" 367 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 368 369 See also: `OverviewAccounts()`, `RequestAccounts()`. 370 """ 371 372 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 373 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 374 375 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 376 377 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 378 """ 379 380 self.iList = None # init iList for raw instruments data 381 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 382 383 See also: `Listing()`, `DumpInstruments()`. 384 """ 385 386 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 387 if useCache: 388 if os.path.exists(self.iListDumpFile): 389 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 390 curTime = datetime.now(tzutc()) 391 392 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 393 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 394 395 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 396 397 else: 398 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 399 400 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 401 os.path.abspath(self.iListDumpFile), 402 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 403 )) 404 405 else: 406 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 407 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 408 409 else: 410 self.iList = self.Listing() # request new raw instruments data from broker server 411 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 412 413 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 414 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 415 416 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 417 """
Main class init.
Parameters
- token: Bearer token for Tinkoff Invest API. It can be set from environment variable
TKS_API_TOKEN. - accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
Also, this variable can be set from environment variable
TKS_ACCOUNT_ID. - useCache: use default cache file with raw data to use instead of
iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then setuseCache=False. - defaultCache: path to default cache file.
dump.jsonby default.
Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
Latest version: https://pypi.org/project/tksbrokerapi/
String with ticker, e.g. GOOGL. Tickers may be upper case only.
Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc.
More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.
See also: SearchByTicker(), SearchInstruments().
String with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6. FIGIs may be upper case only.
See also: SearchByFIGI(), SearchInstruments().
Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.
See also: GetCurrentPrices().
Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().
Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.
See also: SendAPIRequest().
Enables more debug information in this class, such as net request and response headers in all methods. False by default.
Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.
See also: History().
Full path to the html file where rendered candles chart stored. Default: index.html.
See also: ShowHistoryChart().
Filename where full available to user instruments list will be saved. Default: instruments.md.
See also: ShowInstrumentsInfo().
Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.
See also: SearchInstruments().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: GetListOfPrices().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().
Filename where wider Pandas DataFrame with more information about bonds: main info, current prices,
bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.
See also: ExtendBondsData().
Filename where bonds payment calendar will be saved. Default: calendar.md.
Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.
See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().
Filename where current portfolio, open trades and orders will be saved. Default: overview.md.
See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().
Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.
See also: Overview() with parameter details="digest".
Filename where only open positions, without everything else will be saved. Default: overview-positions.md.
See also: Overview() with parameter details="positions".
Filename where open limits and stop orders will be saved. Default: overview-orders.md.
See also: Overview() with parameter details="orders".
Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.
See also: Overview() with parameter details="analytics".
Filename where only the bonds calendar section will be saved. Default: overview-calendar.md.
See also: Overview() with parameter details="calendar".
Filename where history of deals and trade statistics will be saved. Default: deals.md.
See also: Deals().
Filename where table of funds available for withdrawal will be saved. Default: limits.md.
See also: OverviewLimits() and RequestLimits().
Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.
See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().
Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.
See also: OverviewAccounts(), RequestAccounts().
Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.
Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.
See also: DumpInstruments() and DumpInstrumentsAsXLSX().
Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.
See also: Listing(), DumpInstruments().
PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
433 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 434 """ 435 Send GET or POST request to broker server and receive JSON object. 436 437 self.header: must be defining with dictionary of headers. 438 self.body: if define then used as request body. None by default. 439 self.timeout: global request timeout, 15 seconds by default. 440 :param url: url with REST request. 441 :param reqType: send "GET" or "POST" request. "GET" by default. 442 :param retry: how many times retry after first request if an 5xx server errors occurred. 443 :param pause: sleep time in seconds between retries. 444 :return: response JSON (dictionary) from broker. 445 """ 446 if reqType not in ("GET", "POST"): 447 uLogger.error("You can define request type: 'GET' or 'POST'!") 448 raise Exception("Incorrect value") 449 450 if self.moreDebug: 451 uLogger.debug("Request parameters:") 452 uLogger.debug(" - REST API URL: {}".format(url)) 453 uLogger.debug(" - request type: {}".format(reqType)) 454 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 455 uLogger.debug(" - body:\n{}".format(self.body)) 456 457 # fast hack to avoid all operations with some tickers/FIGI 458 responseJSON = {} 459 oK = True 460 for item in self.exclude: 461 if item in url: 462 if self.moreDebug: 463 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 464 465 oK = False 466 break 467 468 if oK: 469 counter = 0 470 response = None 471 errMsg = "" 472 473 while not response and counter <= retry: 474 if reqType == "GET": 475 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 476 477 if reqType == "POST": 478 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 479 480 if self.moreDebug: 481 uLogger.debug("Response:") 482 uLogger.debug(" - status code: {}".format(response.status_code)) 483 uLogger.debug(" - reason: {}".format(response.reason)) 484 uLogger.debug(" - body length: {}".format(len(response.text))) 485 uLogger.debug(" - headers:\n{}".format(response.headers)) 486 487 # Server returns some headers: 488 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 489 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 490 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 491 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 492 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 493 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 494 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 495 sleep(rateLimitWait) 496 497 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 498 if 400 <= response.status_code < 500: 499 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 500 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 501 counter = retry + 1 502 503 if 500 <= response.status_code < 600: 504 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 505 uLogger.debug(" - not oK, {}".format(errMsg)) 506 counter += 1 507 508 if counter <= retry: 509 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 510 sleep(pause) 511 512 responseJSON = self._ParseJSON(rawData=response.text) 513 514 if errMsg: 515 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 516 uLogger.error(" - not oK, {}".format(errMsg)) 517 518 return responseJSON
Send GET or POST request to broker server and receive JSON object.
self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.
Parameters
- url: url with REST request.
- reqType: send "GET" or "POST" request. "GET" by default.
- retry: how many times retry after first request if an 5xx server errors occurred.
- pause: sleep time in seconds between retries.
Returns
response JSON (dictionary) from broker.
551 def Listing(self) -> dict: 552 """ 553 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 554 555 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 556 """ 557 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 558 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 559 560 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 561 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 562 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 563 564 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 565 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 566 poolUpdater.close() 567 568 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 569 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 570 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 571 572 # calculate minimum price increment (step) for all instruments and set up instrument's type: 573 for iType in iList.keys(): 574 for ticker in iList[iType]: 575 iList[iType][ticker]["type"] = iType 576 577 if "minPriceIncrement" in iList[iType][ticker].keys(): 578 iList[iType][ticker]["step"] = NanoToFloat( 579 iList[iType][ticker]["minPriceIncrement"]["units"], 580 iList[iType][ticker]["minPriceIncrement"]["nano"], 581 ) 582 583 else: 584 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 585 586 return iList
Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
Returns
Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
588 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 589 """ 590 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 591 592 See also: `DumpInstruments()`, `Listing()`. 593 594 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 595 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 596 """ 597 if self.iListDumpFile is None or not self.iListDumpFile: 598 uLogger.error("Output name of dump file must be defined!") 599 raise Exception("Filename required") 600 601 if not self.iList or forceUpdate: 602 self.iList = self.Listing() 603 604 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 605 606 # Save as XLSX with separated sheets for every type of instruments: 607 with pd.ExcelWriter( 608 path=xlsxDumpFile, 609 date_format=TKS_DATE_FORMAT, 610 datetime_format=TKS_DATE_TIME_FORMAT, 611 mode="w", 612 ) as writer: 613 for iType in TKS_INSTRUMENTS: 614 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 615 df = df[sorted(df)] # sorted by column names 616 df = df.applymap( 617 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 618 na_action="ignore", 619 ) # converting numbers from nano-type to float in every cell 620 df.to_excel( 621 writer, 622 sheet_name=iType, 623 encoding="UTF-8", 624 freeze_panes=(1, 1), 625 ) # saving as XLSX-file with freeze first row and column as headers 626 627 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
See also: DumpInstruments(), Listing().
Parameters
629 def DumpInstruments(self, forceUpdate: bool = True) -> str: 630 """ 631 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 632 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 633 634 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 635 636 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 637 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 638 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 639 """ 640 if self.iListDumpFile is None or not self.iListDumpFile: 641 uLogger.error("Output name of dump file must be defined!") 642 raise Exception("Filename required") 643 644 if not self.iList or forceUpdate: 645 self.iList = self.Listing() 646 647 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 648 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 649 fH.write(jsonDump) 650 651 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 652 653 return jsonDump
Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
using Listing() method. If iListDumpFile string is not empty then also save information to this file.
See also: DumpInstrumentsAsXLSX(), Listing().
Parameters
- forceUpdate: if
Truethen at first updates data withListing()method, otherwise just saves existiListas JSON-file (default:dump.json).
Returns
serialized JSON formatted
strwith full data of instruments, also saved to the--outputJSON-file.
655 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 656 """ 657 Show information about one instrument defined by json data and prints it in Markdown format. 658 659 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 660 661 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 662 :param show: if `True` then also printing information about instrument and its current price. 663 :return: multilines text in Markdown format with information about one instrument. 664 """ 665 splitLine = "| | |\n" 666 infoText = "" 667 668 if iJSON is not None and iJSON and isinstance(iJSON, dict): 669 info = [ 670 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 671 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 672 "| Parameters | Values |\n", 673 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 674 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 675 "| Full name: | {:<54} |\n".format(iJSON["name"]), 676 ] 677 678 if "sector" in iJSON.keys() and iJSON["sector"]: 679 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 680 681 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 682 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 683 684 info.extend([ 685 splitLine, 686 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 687 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 688 ]) 689 690 if "isin" in iJSON.keys() and iJSON["isin"]: 691 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 692 693 if "classCode" in iJSON.keys(): 694 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 695 696 info.extend([ 697 splitLine, 698 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 699 splitLine, 700 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 701 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 702 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 703 ]) 704 705 if iJSON["figi"]: 706 self.figi = iJSON["figi"] 707 iJSON = iJSON | self.RequestTradingStatus() 708 709 info.extend([ 710 splitLine, 711 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 712 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 713 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 714 ]) 715 716 info.append(splitLine) 717 718 if "type" in iJSON.keys() and iJSON["type"]: 719 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 720 721 if "shareType" in iJSON.keys() and iJSON["shareType"]: 722 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 723 724 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 725 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 726 727 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 728 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 729 730 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 731 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 732 733 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 734 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 735 736 if "focusType" in iJSON.keys() and iJSON["focusType"]: 737 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 738 739 if "assetType" in iJSON.keys() and iJSON["assetType"]: 740 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 741 742 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 743 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 744 745 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 746 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 747 748 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 749 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 750 751 if "currency" in iJSON.keys(): 752 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 753 754 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 755 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 756 757 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 758 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 759 760 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 761 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 762 763 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 764 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 765 766 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 767 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 768 769 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 770 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 771 772 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 773 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 774 775 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 776 info.append("| Perpetual bond: | Yes |\n") 777 778 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 779 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 780 781 iExt = None 782 if iJSON["type"] == "Bonds": 783 info.extend([ 784 splitLine, 785 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 786 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 787 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 788 iJSON["nominal"]["currency"], 789 )), 790 ]) 791 792 if "floatingCouponFlag" in iJSON.keys(): 793 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 794 795 if "amortizationFlag" in iJSON.keys(): 796 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 797 798 info.append(splitLine) 799 800 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 801 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 802 803 if iJSON["figi"]: 804 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 805 806 info.extend([ 807 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 808 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 809 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 810 ]) 811 812 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 813 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 814 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 815 iJSON["aciValue"]["currency"] 816 ))) 817 818 if "currentPrice" in iJSON.keys(): 819 info.append(splitLine) 820 821 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 822 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 823 824 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 825 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 826 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 827 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 828 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 829 830 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 831 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 832 833 info.extend([ 834 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 835 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 836 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 837 )), 838 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 839 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 840 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 841 )), 842 "| Changes between last deal price and last close | {:<54} |\n".format( 843 "{:.2f}%{}".format( 844 iJSON["currentPrice"]["changes"], 845 " ({}{:.2f} {})".format( 846 "+" if bondChangesDelta > 0 else "", 847 bondChangesDelta, 848 aciCurrency 849 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 850 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 851 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 852 currency 853 ), 854 ) 855 ), 856 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 857 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 858 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 859 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 860 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 861 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 862 )), 863 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 864 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 865 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 866 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 867 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 868 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 869 )), 870 ]) 871 872 if "lot" in iJSON.keys(): 873 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 874 875 if "step" in iJSON.keys() and iJSON["step"] != 0: 876 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 877 878 # Add bond payment calendar: 879 if iJSON["type"] == "Bonds": 880 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 881 info.extend(["\n", strCalendar]) 882 883 infoText += "".join(info) 884 885 if show: 886 uLogger.info("{}".format(infoText)) 887 888 else: 889 uLogger.debug("{}".format(infoText)) 890 891 if self.infoFile is not None: 892 with open(self.infoFile, "w", encoding="UTF-8") as fH: 893 fH.write(infoText) 894 895 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 896 897 return infoText
Show information about one instrument defined by json data and prints it in Markdown format.
See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().
Parameters
- iJSON: json data of instrument, example:
iJSON = self.iList["Shares"][self.ticker] - show: if
Truethen also printing information about instrument and its current price.
Returns
multilines text in Markdown format with information about one instrument.
899 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 900 """ 901 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 902 903 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 904 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 905 :return: JSON formatted data with information about instrument. 906 """ 907 tickerJSON = {} 908 if self.moreDebug: 909 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 910 911 if not self.ticker: 912 uLogger.warning("self.ticker variable is not be empty!") 913 914 else: 915 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 916 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 917 raise Exception("Instrument not allowed") 918 919 if not self.iList: 920 self.iList = self.Listing() 921 922 if self.ticker in self.iList["Shares"].keys(): 923 tickerJSON = self.iList["Shares"][self.ticker] 924 if self.moreDebug: 925 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 926 927 elif self.ticker in self.iList["Currencies"].keys(): 928 tickerJSON = self.iList["Currencies"][self.ticker] 929 if self.moreDebug: 930 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 931 932 elif self.ticker in self.iList["Bonds"].keys(): 933 tickerJSON = self.iList["Bonds"][self.ticker] 934 if self.moreDebug: 935 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 936 937 elif self.ticker in self.iList["Etfs"].keys(): 938 tickerJSON = self.iList["Etfs"][self.ticker] 939 if self.moreDebug: 940 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 941 942 elif self.ticker in self.iList["Futures"].keys(): 943 tickerJSON = self.iList["Futures"][self.ticker] 944 if self.moreDebug: 945 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 946 947 if tickerJSON: 948 self.figi = tickerJSON["figi"] 949 950 if requestPrice: 951 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 952 953 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 954 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 955 956 else: 957 tickerJSON["currentPrice"]["changes"] = 0 958 959 if show: 960 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 961 962 else: 963 if show: 964 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 965 966 return tickerJSON
Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (because this is long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
968 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 969 """ 970 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 971 972 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 973 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 974 :return: JSON formatted data with information about instrument. 975 """ 976 figiJSON = {} 977 if self.moreDebug: 978 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 979 980 if not self.figi: 981 uLogger.warning("self.figi variable is not be empty!") 982 983 else: 984 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 985 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 986 raise Exception("Instrument not allowed") 987 988 if not self.iList: 989 self.iList = self.Listing() 990 991 for item in self.iList["Shares"].keys(): 992 if self.figi == self.iList["Shares"][item]["figi"]: 993 figiJSON = self.iList["Shares"][item] 994 995 if self.moreDebug: 996 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 997 998 break 999 1000 if not figiJSON: 1001 for item in self.iList["Currencies"].keys(): 1002 if self.figi == self.iList["Currencies"][item]["figi"]: 1003 figiJSON = self.iList["Currencies"][item] 1004 1005 if self.moreDebug: 1006 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1007 1008 break 1009 1010 if not figiJSON: 1011 for item in self.iList["Bonds"].keys(): 1012 if self.figi == self.iList["Bonds"][item]["figi"]: 1013 figiJSON = self.iList["Bonds"][item] 1014 1015 if self.moreDebug: 1016 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1017 1018 break 1019 1020 if not figiJSON: 1021 for item in self.iList["Etfs"].keys(): 1022 if self.figi == self.iList["Etfs"][item]["figi"]: 1023 figiJSON = self.iList["Etfs"][item] 1024 1025 if self.moreDebug: 1026 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1027 1028 break 1029 1030 if not figiJSON: 1031 for item in self.iList["Futures"].keys(): 1032 if self.figi == self.iList["Futures"][item]["figi"]: 1033 figiJSON = self.iList["Futures"][item] 1034 1035 if self.moreDebug: 1036 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1037 1038 break 1039 1040 if figiJSON: 1041 self.figi = figiJSON["figi"] 1042 self.ticker = figiJSON["ticker"] 1043 1044 if requestPrice: 1045 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1046 1047 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1048 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1049 1050 else: 1051 figiJSON["currentPrice"]["changes"] = 0 1052 1053 if show: 1054 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1055 1056 else: 1057 if show: 1058 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1059 1060 return figiJSON
Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (it's long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
1062 def GetCurrentPrices(self, show: bool = True) -> dict: 1063 """ 1064 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1065 `{"buy": [{"price": 1243.8, "quantity": 193}, 1066 {"price": 1244.0, "quantity": 168}, 1067 {"price": 1244.8, "quantity": 5}, 1068 {"price": 1245.0, "quantity": 61}, 1069 {"price": 1245.4, "quantity": 60}], 1070 "sell": [{"price": 1243.6, "quantity": 8}, 1071 {"price": 1242.6, "quantity": 10}, 1072 {"price": 1242.4, "quantity": 18}, 1073 {"price": 1242.2, "quantity": 50}, 1074 {"price": 1242.0, "quantity": 113}], 1075 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1076 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1077 - sell: list of dicts with Buyers prices, 1078 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1079 - quantity: volume value by current price in lots, 1080 - limitUp: current trade session limit price, maximum, 1081 - limitDown: current trade session limit price, minimum, 1082 - lastPrice: last deal price of the instrument, 1083 - closePrice: previous trade session close price of the instrument. 1084 1085 See also: `SearchByTicker()` and `SearchByFIGI()`. 1086 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1087 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1088 1089 :param show: if `True` then print DOM to log and console. 1090 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1091 If an error occurred then returns an empty record: 1092 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1093 """ 1094 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1095 1096 if self.depth < 1: 1097 uLogger.error("Depth of Market (DOM) must be >=1!") 1098 raise Exception("Incorrect value") 1099 1100 if not (self.ticker or self.figi): 1101 uLogger.error("self.ticker or self.figi variables must be defined!") 1102 raise Exception("Ticker or FIGI required") 1103 1104 if self.ticker and not self.figi: 1105 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1106 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1107 1108 if not self.ticker and self.figi: 1109 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1110 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1111 1112 if not self.figi: 1113 uLogger.error("FIGI is not defined!") 1114 raise Exception("Ticker or FIGI required") 1115 1116 else: 1117 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1118 1119 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1120 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1121 self.body = str({"figi": self.figi, "depth": self.depth}) 1122 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1123 1124 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1125 # list of dicts with sellers orders: 1126 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1127 1128 # list of dicts with buyers orders: 1129 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1130 1131 # max price of instrument at this time: 1132 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1133 1134 # min price of instrument at this time: 1135 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1136 1137 # last price of deal with instrument: 1138 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1139 1140 # last close price of instrument: 1141 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1142 1143 else: 1144 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1145 uLogger.debug("Server response: {}".format(pricesResponse)) 1146 1147 if show: 1148 if prices["buy"] or prices["sell"]: 1149 info = [ 1150 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1151 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1152 self.ticker, 1153 self.figi, 1154 self.depth, 1155 ), 1156 "-" * 60, "\n", 1157 " Orders of Buyers | Orders of Sellers\n", 1158 "-" * 60, "\n", 1159 " Sell prices (volumes) | Buy prices (volumes)\n", 1160 "-" * 60, "\n", 1161 ] 1162 1163 if not prices["buy"]: 1164 info.append(" | No orders!\n") 1165 sumBuy = 0 1166 1167 else: 1168 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1169 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1170 for item in maxMinSorted: 1171 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1172 1173 if not prices["sell"]: 1174 info.append("No orders! |\n") 1175 sumSell = 0 1176 1177 else: 1178 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1179 for item in prices["sell"]: 1180 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1181 1182 info.extend([ 1183 "-" * 60, "\n", 1184 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1185 "-" * 60, "\n", 1186 ]) 1187 1188 infoText = "".join(info) 1189 1190 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1191 1192 else: 1193 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1194 1195 return prices
Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5:
{"buy": [{"price": 1243.8, "quantity": 193},
{"price": 1244.0, "quantity": 168},
{"price": 1244.8, "quantity": 5},
{"price": 1245.0, "quantity": 61},
{"price": 1245.4, "quantity": 60}],
"sell": [{"price": 1243.6, "quantity": 8},
{"price": 1242.6, "quantity": 10},
{"price": 1242.4, "quantity": 18},
{"price": 1242.2, "quantity": 50},
{"price": 1242.0, "quantity": 113}],
"limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:
- buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
- sell: list of dicts with Buyers prices,
- price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
- quantity: volume value by current price in lots,
- limitUp: current trade session limit price, maximum,
- limitDown: current trade session limit price, minimum,
- lastPrice: last deal price of the instrument,
- closePrice: previous trade session close price of the instrument.
See also: SearchByTicker() and SearchByFIGI().
REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
Parameters
- show: if
Truethen print DOM to log and console.
Returns
orders book dict with lists of current buy and sell prices:
{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record:{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.
1197 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1198 """ 1199 This method get and show information about all available broker instruments for current user account. 1200 If `instrumentsFile` string is not empty then also save information to this file. 1201 1202 :param show: if `True` then print results to console, if `False` — print only to file. 1203 :return: multi-lines string with all available broker instruments 1204 """ 1205 if not self.iList: 1206 self.iList = self.Listing() 1207 1208 info = [ 1209 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1210 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1211 ] 1212 1213 # add instruments count by type: 1214 for iType in self.iList.keys(): 1215 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1216 1217 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1218 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1219 1220 # generating info tables with all instruments by type: 1221 for iType in self.iList.keys(): 1222 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1223 1224 for instrument in self.iList[iType].keys(): 1225 iName = self.iList[iType][instrument]["name"] # instrument's name 1226 if len(iName) > 57: 1227 iName = "{}...".format(iName[:54]) # right trim for a long string 1228 1229 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1230 self.iList[iType][instrument]["ticker"], 1231 iName, 1232 self.iList[iType][instrument]["figi"], 1233 self.iList[iType][instrument]["currency"], 1234 self.iList[iType][instrument]["lot"], 1235 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1236 )) 1237 1238 infoText = "".join(info) 1239 1240 if show: 1241 uLogger.info(infoText) 1242 1243 if self.instrumentsFile: 1244 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1245 fH.write(infoText) 1246 1247 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1248 1249 return infoText
This method get and show information about all available broker instruments for current user account.
If instrumentsFile string is not empty then also save information to this file.
Parameters
- show: if
Truethen print results to console, ifFalse— print only to file.
Returns
multi-lines string with all available broker instruments
1251 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1252 """ 1253 This method search and show information about instruments by part of its ticker, FIGI or name. 1254 If `searchResultsFile` string is not empty then also save information to this file. 1255 1256 :param pattern: string with part of ticker, FIGI or instrument's name. 1257 :param show: if `True` then print results to console, if `False` — return list of result only. 1258 :return: list of dictionaries with all found instruments. 1259 """ 1260 if not self.iList: 1261 self.iList = self.Listing() 1262 1263 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1264 compiledPattern = re.compile(pattern, re.IGNORECASE) 1265 1266 for iType in self.iList: 1267 for instrument in self.iList[iType].values(): 1268 searchResult = compiledPattern.search(" ".join( 1269 [instrument["ticker"], instrument["figi"], instrument["name"]] 1270 )) 1271 1272 if searchResult: 1273 searchResults[iType][instrument["ticker"]] = instrument 1274 1275 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1276 info = [ 1277 "# Search results\n\n", 1278 "* **Search pattern:** [{}]\n".format(pattern), 1279 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1280 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1281 ] 1282 infoShort = info[:] 1283 1284 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1285 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1286 skippedLine = "| ... | ... | ... | ... |\n" 1287 1288 if resultsLen == 0: 1289 info.append("\nNo results\n") 1290 infoShort.append("\nNo results\n") 1291 uLogger.warning("No results. Try changing your search pattern.") 1292 1293 else: 1294 for iType in searchResults: 1295 iTypeValuesCount = len(searchResults[iType].values()) 1296 if iTypeValuesCount > 0: 1297 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1298 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1299 1300 for instrument in searchResults[iType].values(): 1301 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1302 instrument["type"], 1303 instrument["ticker"], 1304 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1305 instrument["figi"], 1306 )) 1307 1308 if iTypeValuesCount <= 5: 1309 infoShort.extend(info[-iTypeValuesCount:]) 1310 1311 else: 1312 infoShort.extend(info[-5:]) 1313 infoShort.append(skippedLine) 1314 1315 infoText = "".join(info) 1316 infoTextShort = "".join(infoShort) 1317 1318 if show: 1319 uLogger.info(infoTextShort) 1320 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1321 1322 if self.searchResultsFile: 1323 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1324 fH.write(infoText) 1325 1326 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1327 1328 return searchResults
This method search and show information about instruments by part of its ticker, FIGI or name.
If searchResultsFile string is not empty then also save information to this file.
Parameters
- pattern: string with part of ticker, FIGI or instrument's name.
- show: if
Truethen print results to console, ifFalse— return list of result only.
Returns
list of dictionaries with all found instruments.
1330 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1331 """ 1332 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1333 1334 :param instruments: list of strings with tickers or FIGIs. 1335 :return: list with unique instrument FIGIs only. 1336 """ 1337 requestedInstruments = [] 1338 for iName in instruments: 1339 if iName not in self.aliases.keys(): 1340 if iName not in requestedInstruments: 1341 requestedInstruments.append(iName) 1342 1343 else: 1344 if iName not in requestedInstruments: 1345 if self.aliases[iName] not in requestedInstruments: 1346 requestedInstruments.append(self.aliases[iName]) 1347 1348 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1349 1350 onlyUniqueFIGIs = [] 1351 for iName in requestedInstruments: 1352 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1353 continue 1354 1355 self.ticker = iName 1356 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1357 1358 if not iData: 1359 self.ticker = "" 1360 self.figi = iName 1361 1362 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1363 1364 if not iData: 1365 self.figi = "" 1366 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1367 1368 if iData and iData["figi"] not in onlyUniqueFIGIs: 1369 onlyUniqueFIGIs.append(iData["figi"]) 1370 1371 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1372 1373 return onlyUniqueFIGIs
Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
Parameters
- instruments: list of strings with tickers or FIGIs.
Returns
list with unique instrument FIGIs only.
1375 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1376 """ 1377 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1378 1379 See limits: https://tinkoff.github.io/investAPI/limits/ 1380 1381 If `pricesFile` string is not empty then also save information to this file. 1382 1383 :param instruments: list of strings with tickers or FIGIs. 1384 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1385 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1386 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1387 """ 1388 if instruments is None or not instruments: 1389 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1390 raise Exception("Ticker or FIGI required") 1391 1392 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1393 1394 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1395 1396 iList = [] # trying to get info and current prices about all unique instruments: 1397 for self.figi in onlyUniqueFIGIs: 1398 iData = self.SearchByFIGI(requestPrice=True) 1399 iList.append(iData) 1400 1401 self.ShowListOfPrices(iList, show) 1402 1403 return iList
This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
See limits: https://tinkoff.github.io/investAPI/limits/
If pricesFile string is not empty then also save information to this file.
Parameters
- instruments: list of strings with tickers or FIGIs.
- show: if
Truethen prints prices to console, ifFalse— prints only to filepricesFile.
Returns
list of instruments looks like
[{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker()orSearchByFIGI()methods.
1405 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1406 """ 1407 Show table contains current prices of given instruments. 1408 1409 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1410 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1411 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1412 :return: multilines text in Markdown format as a table contains current prices. 1413 """ 1414 infoText = "" 1415 1416 if show or self.pricesFile: 1417 info = [ 1418 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1419 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1420 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1421 ] 1422 1423 for item in iList: 1424 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1425 item["ticker"], 1426 item["figi"], 1427 item["type"], 1428 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1429 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1430 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1431 "{} / {}".format( 1432 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1433 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1434 ), 1435 "{} / {}".format( 1436 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1437 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1438 ), 1439 item["currency"], 1440 )) 1441 1442 infoText = "".join(info) 1443 1444 if show: 1445 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1446 1447 if self.pricesFile: 1448 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1449 fH.write(infoText) 1450 1451 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1452 1453 return infoText
Show table contains current prices of given instruments.
Parameters
- **iList: list of instruments looks like
[{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker(requestPrice=True)or bySearchByFIGI(requestPrice=True)methods. - show: if
Truethen prints prices to console, ifFalse— prints only to filepricesFile.
Returns
multilines text in Markdown format as a table contains current prices.
1455 def RequestTradingStatus(self) -> dict: 1456 """ 1457 Requesting trading status for the instrument defined by `figi` variable. 1458 1459 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1460 1461 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1462 1463 :return: dictionary with trading status attributes. Response example: 1464 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1465 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1466 """ 1467 if self.figi is None or not self.figi: 1468 uLogger.error("Variable `figi` must be defined for using this method!") 1469 raise Exception("FIGI required") 1470 1471 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1472 1473 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1474 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1475 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1476 1477 if self.moreDebug: 1478 uLogger.debug("Records about current trading status successfully received") 1479 1480 return tradingStatus
Requesting trading status for the instrument defined by figi variable.
Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
Returns
dictionary with trading status attributes. Response example:
{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}
1482 def RequestPortfolio(self) -> dict: 1483 """ 1484 Requesting actual user's portfolio for current `accountId`. 1485 1486 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1487 1488 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1489 1490 :return: dictionary with user's portfolio. 1491 """ 1492 if self.accountId is None or not self.accountId: 1493 uLogger.error("Variable `accountId` must be defined for using this method!") 1494 raise Exception("Account ID required") 1495 1496 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1497 1498 self.body = str({"accountId": self.accountId}) 1499 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1500 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1501 1502 if self.moreDebug: 1503 uLogger.debug("Records about user's portfolio successfully received") 1504 1505 return rawPortfolio
Requesting actual user's portfolio for current accountId.
REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
Returns
dictionary with user's portfolio.
1507 def RequestPositions(self) -> dict: 1508 """ 1509 Requesting open positions by currencies and instruments for current `accountId`. 1510 1511 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1512 1513 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1514 1515 :return: dictionary with open positions by instruments. 1516 """ 1517 if self.accountId is None or not self.accountId: 1518 uLogger.error("Variable `accountId` must be defined for using this method!") 1519 raise Exception("Account ID required") 1520 1521 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1522 1523 self.body = str({"accountId": self.accountId}) 1524 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1525 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1526 1527 if self.moreDebug: 1528 uLogger.debug("Records about current open positions successfully received") 1529 1530 return rawPositions
Requesting open positions by currencies and instruments for current accountId.
REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
Returns
dictionary with open positions by instruments.
1532 def RequestPendingOrders(self) -> list: 1533 """ 1534 Requesting current actual pending orders for current `accountId`. 1535 1536 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1537 1538 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1539 1540 :return: list of dictionaries with pending orders. 1541 """ 1542 if self.accountId is None or not self.accountId: 1543 uLogger.error("Variable `accountId` must be defined for using this method!") 1544 raise Exception("Account ID required") 1545 1546 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1547 1548 self.body = str({"accountId": self.accountId}) 1549 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1550 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1551 1552 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1553 1554 return rawOrders
Requesting current actual pending orders for current accountId.
REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
Returns
list of dictionaries with pending orders.
1556 def RequestStopOrders(self) -> list: 1557 """ 1558 Requesting current actual stop orders for current `accountId`. 1559 1560 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1561 1562 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1563 1564 :return: list of dictionaries with stop orders. 1565 """ 1566 if self.accountId is None or not self.accountId: 1567 uLogger.error("Variable `accountId` must be defined for using this method!") 1568 raise Exception("Account ID required") 1569 1570 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1571 1572 self.body = str({"accountId": self.accountId}) 1573 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1574 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1575 1576 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1577 1578 return rawStopOrders
Requesting current actual stop orders for current accountId.
REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
Returns
list of dictionaries with stop orders.
1580 def Overview(self, show: bool = False, details: str = "full") -> dict: 1581 """ 1582 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1583 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1584 and `overviewBondsCalendarFile` are defined then also save information to file. 1585 1586 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1587 many requests about the state of the portfolio, and then, based on the received data, a large number 1588 of calculation and statistics are collected. 1589 1590 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1591 :param details: how detailed should the information be? 1592 - `full` — shows full available information about portfolio status (by default), 1593 - `positions` — shows only open positions, 1594 - `orders` — shows only sections of open limits and stop orders. 1595 - `digest` — show a short digest of the portfolio status, 1596 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1597 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1598 :return: dictionary with client's raw portfolio and some statistics. 1599 """ 1600 if self.accountId is None or not self.accountId: 1601 uLogger.error("Variable `accountId` must be defined for using this method!") 1602 raise Exception("Account ID required") 1603 1604 view = { 1605 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1606 "headers": {}, # list of dictionaries, response headers without "positions" section 1607 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1608 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1609 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1610 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1611 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1612 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1613 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1614 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1615 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1616 }, 1617 "stat": { # --- some statistics calculated using "raw" sections: 1618 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1619 "availableRUB": 0., # available rubles (without other currencies) 1620 "blockedRUB": 0., # blocked sum in Russian Rouble 1621 "totalChangesRUB": 0., # changes for all open trades in RUB 1622 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1623 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1624 "sharesCostRUB": 0., # costs of all shares in RUB 1625 "bondsCostRUB": 0., # costs of all bonds in RUB 1626 "etfsCostRUB": 0., # costs of all etfs in RUB 1627 "futuresCostRUB": 0., # costs of all futures in RUB 1628 "Currencies": [], # list of dictionaries of all currencies statistics 1629 "Shares": [], # list of dictionaries of all shares statistics 1630 "Bonds": [], # list of dictionaries of all bonds statistics 1631 "Etfs": [], # list of dictionaries of all etfs statistics 1632 "Futures": [], # list of dictionaries of all futures statistics 1633 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1634 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1635 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1636 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1637 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1638 }, 1639 "analytics": { # --- some analytics of portfolio: 1640 "distrByAssets": {}, # portfolio distribution by assets 1641 "distrByCompanies": {}, # portfolio distribution by companies 1642 "distrBySectors": {}, # portfolio distribution by sectors 1643 "distrByCurrencies": {}, # portfolio distribution by currencies 1644 "distrByCountries": {}, # portfolio distribution by countries 1645 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1646 } 1647 } 1648 1649 details = details.lower() 1650 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1651 if details not in availableDetails: 1652 details = "full" 1653 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1654 1655 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1656 1657 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1658 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1659 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1660 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1661 1662 # save response headers without "positions" section: 1663 for key in portfolioResponse.keys(): 1664 if key != "positions": 1665 view["raw"]["headers"][key] = portfolioResponse[key] 1666 1667 else: 1668 continue 1669 1670 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1671 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1672 for item in portfolioResponse["positions"]: 1673 if item["instrumentType"] == "currency": 1674 self.figi = item["figi"] 1675 curr = self.SearchByFIGI(requestPrice=False) 1676 1677 # current price of currency in RUB: 1678 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1679 "name": curr["name"], 1680 "currentPrice": NanoToFloat( 1681 item["currentPrice"]["units"], 1682 item["currentPrice"]["nano"] 1683 ), 1684 } 1685 1686 view["raw"]["Currencies"].append(item) 1687 1688 elif item["instrumentType"] == "share": 1689 view["raw"]["Shares"].append(item) 1690 1691 elif item["instrumentType"] == "bond": 1692 view["raw"]["Bonds"].append(item) 1693 1694 elif item["instrumentType"] == "etf": 1695 view["raw"]["Etfs"].append(item) 1696 1697 elif item["instrumentType"] == "futures": 1698 view["raw"]["Futures"].append(item) 1699 1700 else: 1701 continue 1702 1703 # how many volume of currencies (by ISO currency name) are blocked: 1704 for item in view["raw"]["positions"]["blocked"]: 1705 blocked = NanoToFloat(item["units"], item["nano"]) 1706 if blocked > 0: 1707 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1708 1709 # how many volume of instruments (by FIGI) are blocked: 1710 for item in view["raw"]["positions"]["securities"]: 1711 blocked = int(item["blocked"]) 1712 if blocked > 0: 1713 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1714 1715 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1716 1717 if "rub" in allBlocked.keys(): 1718 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1719 1720 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1721 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1722 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1723 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1724 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1725 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1726 view["stat"]["portfolioCostRUB"] = sum([ 1727 view["stat"]["allCurrenciesCostRUB"], 1728 view["stat"]["sharesCostRUB"], 1729 view["stat"]["bondsCostRUB"], 1730 view["stat"]["etfsCostRUB"], 1731 view["stat"]["futuresCostRUB"], 1732 ]) 1733 1734 # --- calculating some portfolio statistics: 1735 byComp = {} # distribution by companies 1736 bySect = {} # distribution by sectors 1737 byCurr = {} # distribution by currencies (include RUB) 1738 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1739 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1740 1741 for item in portfolioResponse["positions"]: 1742 self.figi = item["figi"] 1743 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1744 1745 if instrument: 1746 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1747 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1748 1749 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1750 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1751 1752 else: 1753 blocked = 0 1754 1755 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1756 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1757 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1758 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1759 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1760 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1761 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1762 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1763 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1764 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1765 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1766 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1767 1768 statData = { 1769 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1770 "ticker": instrument["ticker"], # ticker by FIGI 1771 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1772 "volume": volume, # available volume of instrument 1773 "lots": lots, # volume in lots of instrument 1774 "direction": direction, # direction of an instrument's position: short or long 1775 "blocked": blocked, # blocked volume of currency or instrument 1776 "currentPrice": curPrice, # current instrument's price in basic asset 1777 "average": average, # current average position price 1778 "cost": cost, # current cost of all volume of instrument in basic asset 1779 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1780 "costRUB": costRUB, # cost of instrument in ruble 1781 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1782 "profit": profit, # expected profit at current moment 1783 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1784 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1785 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1786 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1787 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1788 "step": instrument["step"], # minimum price increment 1789 } 1790 1791 # adding distribution by unique countries: 1792 if statData["country"] not in byCountry.keys(): 1793 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1794 1795 else: 1796 byCountry[statData["country"]]["cost"] += costRUB 1797 byCountry[statData["country"]]["percent"] += percentCostRUB 1798 1799 if item["instrumentType"] != "currency": 1800 # adding distribution by unique companies: 1801 if statData["name"]: 1802 if statData["name"] not in byComp.keys(): 1803 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1804 1805 else: 1806 byComp[statData["name"]]["cost"] += costRUB 1807 byComp[statData["name"]]["percent"] += percentCostRUB 1808 1809 # adding distribution by unique sectors: 1810 if statData["sector"] not in bySect.keys(): 1811 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1812 1813 else: 1814 bySect[statData["sector"]]["cost"] += costRUB 1815 bySect[statData["sector"]]["percent"] += percentCostRUB 1816 1817 # adding distribution by unique currencies: 1818 if currency not in byCurr.keys(): 1819 byCurr[currency] = { 1820 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1821 "cost": costRUB, 1822 "percent": percentCostRUB 1823 } 1824 1825 else: 1826 byCurr[currency]["cost"] += costRUB 1827 byCurr[currency]["percent"] += percentCostRUB 1828 1829 # saving statistics for every instrument: 1830 if item["instrumentType"] == "currency": 1831 view["stat"]["Currencies"].append(statData) 1832 1833 # update dict with free funds for trading (total - blocked) by currencies 1834 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1835 view["stat"]["funds"][currency] = { 1836 "total": volume, 1837 "totalCostRUB": costRUB, # total volume cost in rubles 1838 "free": volume - blocked, 1839 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1840 } 1841 1842 elif item["instrumentType"] == "share": 1843 view["stat"]["Shares"].append(statData) 1844 1845 elif item["instrumentType"] == "bond": 1846 view["stat"]["Bonds"].append(statData) 1847 1848 elif item["instrumentType"] == "etf": 1849 view["stat"]["Etfs"].append(statData) 1850 1851 elif item["instrumentType"] == "Futures": 1852 view["stat"]["Futures"].append(statData) 1853 1854 else: 1855 continue 1856 1857 # total changes in Russian Ruble: 1858 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1859 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1860 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1861 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1862 view["stat"]["funds"]["rub"] = { 1863 "total": view["stat"]["availableRUB"], 1864 "totalCostRUB": view["stat"]["availableRUB"], 1865 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1866 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1867 } 1868 1869 # --- pending orders sector data: 1870 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending orders to avoid many times price requests 1871 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1872 1873 for item in view["raw"]["orders"]: 1874 self.figi = item["figi"] 1875 1876 if item["figi"] not in uniquePendingOrdersFIGIs: 1877 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1878 1879 uniquePendingOrdersFIGIs.append(item["figi"]) 1880 uniquePendingOrders[item["figi"]] = instrument 1881 1882 else: 1883 instrument = uniquePendingOrders[item["figi"]] 1884 1885 if instrument: 1886 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1887 orderType = TKS_ORDER_TYPES[item["orderType"]] 1888 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1889 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1890 1891 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1892 if item["direction"] == "ORDER_DIRECTION_BUY": 1893 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1894 1895 else: 1896 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1897 1898 # requested price for order execution: 1899 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1900 1901 # necessary changes in percent to reach target from current price: 1902 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1903 1904 view["stat"]["orders"].append({ 1905 "orderID": item["orderId"], # orderId number parameter of current order 1906 "figi": item["figi"], # FIGI identification 1907 "ticker": instrument["ticker"], # ticker name by FIGI 1908 "lotsRequested": item["lotsRequested"], # requested lots value 1909 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1910 "currentPrice": lastPrice, # current instrument's price for defined action 1911 "targetPrice": target, # requested price for order execution in base currency 1912 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1913 "percentChanges": changes, # changes in percent to target from current price 1914 "currency": item["currency"], # instrument's currency name 1915 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1916 "type": orderType, # type of order from TKS_ORDER_TYPES 1917 "status": orderState, # order status from TKS_ORDER_STATES 1918 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1919 }) 1920 1921 # --- stop orders sector data: 1922 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1923 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1924 1925 for item in view["raw"]["stopOrders"]: 1926 self.figi = item["figi"] 1927 1928 if item["figi"] not in uniqueStopOrdersFIGIs: 1929 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1930 1931 uniqueStopOrdersFIGIs.append(item["figi"]) 1932 uniqueStopOrders[item["figi"]] = instrument 1933 1934 else: 1935 instrument = uniqueStopOrders[item["figi"]] 1936 1937 if instrument: 1938 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1939 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1940 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1941 1942 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1943 if "expirationTime" in item.keys(): 1944 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1945 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1946 1947 else: 1948 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1949 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1950 1951 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1952 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1953 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1954 1955 else: 1956 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1957 1958 # requested price when stop-order executed: 1959 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1960 1961 # price for limit-order, set up when stop-order executed: 1962 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1963 1964 # necessary changes in percent to reach target from current price: 1965 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1966 1967 view["stat"]["stopOrders"].append({ 1968 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1969 "figi": item["figi"], # FIGI identification 1970 "ticker": instrument["ticker"], # ticker name by FIGI 1971 "lotsRequested": item["lotsRequested"], # requested lots value 1972 "currentPrice": lastPrice, # current instrument's price for defined action 1973 "targetPrice": target, # requested price for stop-order execution in base currency 1974 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1975 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1976 "percentChanges": changes, # changes in percent to target from current price 1977 "currency": item["currency"], # instrument's currency name 1978 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1979 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1980 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1981 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1982 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 1983 }) 1984 1985 # --- calculating data for analytics section: 1986 # portfolio distribution by assets: 1987 view["analytics"]["distrByAssets"] = { 1988 "Ruble": { 1989 "uniques": 1, 1990 "cost": view["stat"]["availableRUB"], 1991 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1992 }, 1993 "Currencies": { 1994 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 1995 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 1996 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1997 }, 1998 "Shares": { 1999 "uniques": len(view["stat"]["Shares"]), 2000 "cost": view["stat"]["sharesCostRUB"], 2001 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2002 }, 2003 "Bonds": { 2004 "uniques": len(view["stat"]["Bonds"]), 2005 "cost": view["stat"]["bondsCostRUB"], 2006 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2007 }, 2008 "Etfs": { 2009 "uniques": len(view["stat"]["Etfs"]), 2010 "cost": view["stat"]["etfsCostRUB"], 2011 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2012 }, 2013 "Futures": { 2014 "uniques": len(view["stat"]["Futures"]), 2015 "cost": view["stat"]["futuresCostRUB"], 2016 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2017 }, 2018 } 2019 2020 # portfolio distribution by companies: 2021 view["analytics"]["distrByCompanies"]["All money cash"] = { 2022 "ticker": "", 2023 "cost": view["stat"]["allCurrenciesCostRUB"], 2024 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2025 } 2026 view["analytics"]["distrByCompanies"].update(byComp) 2027 2028 # portfolio distribution by sectors: 2029 view["analytics"]["distrBySectors"]["All money cash"] = { 2030 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2031 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2032 } 2033 view["analytics"]["distrBySectors"].update(bySect) 2034 2035 # portfolio distribution by currencies: 2036 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2037 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2038 2039 if self.moreDebug: 2040 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2041 2042 view["analytics"]["distrByCurrencies"].update(byCurr) 2043 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2044 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2045 2046 # portfolio distribution by countries: 2047 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2048 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2049 2050 if self.moreDebug: 2051 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2052 2053 view["analytics"]["distrByCountries"].update(byCountry) 2054 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2055 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2056 2057 # --- Prepare text statistics overview in human-readable: 2058 if show: 2059 # Whatever the value `details`, header not changes: 2060 info = [ 2061 "# Client's portfolio\n\n", 2062 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2063 "* **Account ID:** [{}]\n".format(self.accountId), 2064 ] 2065 2066 if details in ["full", "positions", "digest"]: 2067 info.extend([ 2068 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2069 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2070 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2071 view["stat"]["totalChangesRUB"], 2072 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2073 view["stat"]["totalChangesPercentRUB"], 2074 ), 2075 ]) 2076 2077 if details in ["full", "positions"]: 2078 info.extend([ 2079 "## Open positions\n\n", 2080 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2081 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2082 "| Ruble | {:>31} | | | | | |\n".format( 2083 "{:.2f} ({:.2f}) rub".format( 2084 view["stat"]["availableRUB"], 2085 view["stat"]["blockedRUB"], 2086 ) 2087 ) 2088 ]) 2089 2090 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2091 return [ 2092 "| | | | | | | |\n", 2093 "| {:<27} | | | | | {:>19} | |\n".format( 2094 noTradeStr if noTradeStr else typeStr, 2095 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2096 ), 2097 ] 2098 2099 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2100 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2101 "{} [{}]".format(data["ticker"], data["figi"]), 2102 "{:.2f} ({:.2f}) {}".format( 2103 data["volume"], 2104 data["blocked"], 2105 data["currency"], 2106 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2107 data["volume"], 2108 data["blocked"], 2109 ), 2110 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2111 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2112 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2113 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2114 "{}{:.2f} {} ({}{:.2f}%)".format( 2115 "+" if data["profit"] > 0 else "", 2116 data["profit"], data["baseCurrencyName"], 2117 "+" if data["percentProfit"] > 0 else "", 2118 data["percentProfit"], 2119 ), 2120 ) 2121 2122 # --- Show currencies section: 2123 if view["stat"]["Currencies"]: 2124 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2125 for item in view["stat"]["Currencies"]: 2126 info.append(_InfoStr(item, showCurrencyName=True)) 2127 2128 else: 2129 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2130 2131 # --- Show shares section: 2132 if view["stat"]["Shares"]: 2133 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2134 2135 for item in view["stat"]["Shares"]: 2136 info.append(_InfoStr(item)) 2137 2138 else: 2139 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2140 2141 # --- Show bonds section: 2142 if view["stat"]["Bonds"]: 2143 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2144 2145 for item in view["stat"]["Bonds"]: 2146 info.append(_InfoStr(item)) 2147 2148 else: 2149 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2150 2151 # --- Show etfs section: 2152 if view["stat"]["Etfs"]: 2153 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2154 2155 for item in view["stat"]["Etfs"]: 2156 info.append(_InfoStr(item)) 2157 2158 else: 2159 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2160 2161 # --- Show futures section: 2162 if view["stat"]["Futures"]: 2163 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2164 2165 for item in view["stat"]["Futures"]: 2166 info.append(_InfoStr(item)) 2167 2168 else: 2169 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2170 2171 if details in ["full", "orders"]: 2172 # --- Show pending orders section: 2173 if view["stat"]["orders"]: 2174 info.extend([ 2175 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2176 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2177 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2178 ]) 2179 2180 for item in view["stat"]["orders"]: 2181 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2182 "{} [{}]".format(item["ticker"], item["figi"]), 2183 item["orderID"], 2184 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2185 "{} {} ({}{:.2f}%)".format( 2186 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2187 item["baseCurrencyName"], 2188 "+" if item["percentChanges"] > 0 else "", 2189 float(item["percentChanges"]), 2190 ), 2191 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2192 item["action"], 2193 item["type"], 2194 item["date"], 2195 )) 2196 2197 else: 2198 info.append("\n## Total pending limit-orders: 0\n") 2199 2200 # --- Show stop orders section: 2201 if view["stat"]["stopOrders"]: 2202 info.extend([ 2203 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2204 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2205 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2206 ]) 2207 2208 for item in view["stat"]["stopOrders"]: 2209 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2210 "{} [{}]".format(item["ticker"], item["figi"]), 2211 item["orderID"], 2212 item["lotsRequested"], 2213 "{} {} ({}{:.2f}%)".format( 2214 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2215 item["baseCurrencyName"], 2216 "+" if item["percentChanges"] > 0 else "", 2217 float(item["percentChanges"]), 2218 ), 2219 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2220 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2221 item["action"], 2222 item["type"], 2223 item["expType"], 2224 item["createDate"], 2225 item["expDate"], 2226 )) 2227 2228 else: 2229 info.append("\n## Total stop-orders: 0\n") 2230 2231 if details in ["full", "analytics"]: 2232 # -- Show analytics section: 2233 if view["stat"]["portfolioCostRUB"] > 0: 2234 info.extend([ 2235 "\n# Analytics\n" 2236 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2237 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2238 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2239 view["stat"]["totalChangesRUB"], 2240 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2241 view["stat"]["totalChangesPercentRUB"], 2242 ), 2243 "\n## Portfolio distribution by assets\n" 2244 "\n| Type | Uniques | Percent | Current cost |\n", 2245 "|------------------------------------|---------|---------|--------------------|\n", 2246 ]) 2247 2248 for key in view["analytics"]["distrByAssets"].keys(): 2249 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2250 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2251 key, 2252 view["analytics"]["distrByAssets"][key]["uniques"], 2253 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2254 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2255 )) 2256 2257 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2258 2259 info.extend([ 2260 "\n## Portfolio distribution by companies\n" 2261 "\n| Company | Percent | Current cost |\n", 2262 aSepLine, 2263 ]) 2264 2265 for company in view["analytics"]["distrByCompanies"].keys(): 2266 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2267 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2268 "{}{}".format( 2269 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2270 company, 2271 ), 2272 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2273 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2274 )) 2275 2276 info.extend([ 2277 "\n## Portfolio distribution by sectors\n" 2278 "\n| Sector | Percent | Current cost |\n", 2279 aSepLine, 2280 ]) 2281 2282 for sector in view["analytics"]["distrBySectors"].keys(): 2283 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2284 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2285 sector, 2286 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2287 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2288 )) 2289 2290 info.extend([ 2291 "\n## Portfolio distribution by currencies\n" 2292 "\n| Instruments currencies | Percent | Current cost |\n", 2293 aSepLine, 2294 ]) 2295 2296 for curr in view["analytics"]["distrByCurrencies"].keys(): 2297 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2298 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2299 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2300 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2301 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2302 )) 2303 2304 info.extend([ 2305 "\n## Portfolio distribution by countries\n" 2306 "\n| Assets by country | Percent | Current cost |\n", 2307 aSepLine, 2308 ]) 2309 2310 for country in view["analytics"]["distrByCountries"].keys(): 2311 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2312 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2313 country, 2314 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2315 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2316 )) 2317 2318 if details in ["full", "calendar"]: 2319 # -- Show bonds payment calendar section: 2320 if view["stat"]["Bonds"]: 2321 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2322 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2323 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2324 2325 else: 2326 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2327 2328 infoText = "".join(info) 2329 2330 uLogger.info(infoText) 2331 2332 if details == "full" and self.overviewFile: 2333 filename = self.overviewFile 2334 2335 elif details == "digest" and self.overviewDigestFile: 2336 filename = self.overviewDigestFile 2337 2338 elif details == "positions" and self.overviewPositionsFile: 2339 filename = self.overviewPositionsFile 2340 2341 elif details == "orders" and self.overviewOrdersFile: 2342 filename = self.overviewOrdersFile 2343 2344 elif details == "analytics" and self.overviewAnalyticsFile: 2345 filename = self.overviewAnalyticsFile 2346 2347 elif details == "calendar" and self.overviewBondsCalendarFile: 2348 filename = self.overviewBondsCalendarFile 2349 2350 else: 2351 filename = "" 2352 2353 if filename: 2354 with open(filename, "w", encoding="UTF-8") as fH: 2355 fH.write(infoText) 2356 2357 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2358 2359 return view
Get portfolio: all open positions, orders and some statistics for current accountId.
If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile
and overviewBondsCalendarFile are defined then also save information to file.
WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen show more debug information. - details: how detailed should the information be?
full— shows full available information about portfolio status (by default),positions— shows only open positions,orders— shows only sections of open limits and stop orders.digest— show a short digest of the portfolio status,analytics— shows only the analytics section and the distribution of the portfolio by various categories,calendar— shows only the bonds calendar section (if these present in portfolio),
Returns
dictionary with client's raw portfolio and some statistics.
2361 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2362 """ 2363 Returns history operations between two given dates for current `accountId`. 2364 If `reportFile` string is not empty then also save human-readable report. 2365 Shows some statistical data of closed positions. 2366 2367 :param start: see docstring in `GetDatesAsString()` method 2368 :param end: see docstring in `GetDatesAsString()` method 2369 :param show: if `True` then also prints all records to the console. 2370 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2371 :return: original list of dictionaries with history of deals records from API ("operations" key): 2372 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2373 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2374 """ 2375 if self.accountId is None or not self.accountId: 2376 uLogger.error("Variable `accountId` must be defined for using this method!") 2377 raise Exception("Account ID required") 2378 2379 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2380 2381 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2382 2383 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2384 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2385 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2386 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2387 customStat = {} # custom statistics in additional to responseJSON 2388 2389 # --- output report in human-readable format: 2390 if show or self.reportFile: 2391 splitLine1 = "| | | | | |\n" # Summary section 2392 splitLine2 = "| | | | | | | | |\n" # Operations section 2393 nextDay = "" 2394 2395 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2396 2397 if len(ops) > 0: 2398 customStat = { 2399 "opsCount": 0, # total operations count 2400 "buyCount": 0, # buy operations 2401 "sellCount": 0, # sell operations 2402 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2403 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2404 "payIn": {"rub": 0.}, # Deposit brokerage account 2405 "payOut": {"rub": 0.}, # Withdrawals 2406 "divs": {"rub": 0.}, # Dividends income 2407 "coupons": {"rub": 0.}, # Coupon's income 2408 "brokerCom": {"rub": 0.}, # Service commissions 2409 "serviceCom": {"rub": 0.}, # Service commissions 2410 "marginCom": {"rub": 0.}, # Margin commissions 2411 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2412 } 2413 2414 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2415 for item in ops: 2416 if item["state"] == "OPERATION_STATE_EXECUTED": 2417 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2418 2419 # count buy operations: 2420 if "_BUY" in item["operationType"]: 2421 customStat["buyCount"] += 1 2422 2423 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2424 customStat["buyTotal"][item["payment"]["currency"]] += payment 2425 2426 else: 2427 customStat["buyTotal"][item["payment"]["currency"]] = payment 2428 2429 # count sell operations: 2430 elif "_SELL" in item["operationType"]: 2431 customStat["sellCount"] += 1 2432 2433 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2434 customStat["sellTotal"][item["payment"]["currency"]] += payment 2435 2436 else: 2437 customStat["sellTotal"][item["payment"]["currency"]] = payment 2438 2439 # count incoming operations: 2440 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2441 if item["payment"]["currency"] in customStat["payIn"].keys(): 2442 customStat["payIn"][item["payment"]["currency"]] += payment 2443 2444 else: 2445 customStat["payIn"][item["payment"]["currency"]] = payment 2446 2447 # count withdrawals operations: 2448 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2449 if item["payment"]["currency"] in customStat["payOut"].keys(): 2450 customStat["payOut"][item["payment"]["currency"]] += payment 2451 2452 else: 2453 customStat["payOut"][item["payment"]["currency"]] = payment 2454 2455 # count dividends income: 2456 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2457 if item["payment"]["currency"] in customStat["divs"].keys(): 2458 customStat["divs"][item["payment"]["currency"]] += payment 2459 2460 else: 2461 customStat["divs"][item["payment"]["currency"]] = payment 2462 2463 # count coupon's income: 2464 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2465 if item["payment"]["currency"] in customStat["coupons"].keys(): 2466 customStat["coupons"][item["payment"]["currency"]] += payment 2467 2468 else: 2469 customStat["coupons"][item["payment"]["currency"]] = payment 2470 2471 # count broker commissions: 2472 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2473 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2474 customStat["brokerCom"][item["payment"]["currency"]] += payment 2475 2476 else: 2477 customStat["brokerCom"][item["payment"]["currency"]] = payment 2478 2479 # count service commissions: 2480 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2481 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2482 customStat["serviceCom"][item["payment"]["currency"]] += payment 2483 2484 else: 2485 customStat["serviceCom"][item["payment"]["currency"]] = payment 2486 2487 # count margin commissions: 2488 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2489 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2490 customStat["marginCom"][item["payment"]["currency"]] += payment 2491 2492 else: 2493 customStat["marginCom"][item["payment"]["currency"]] = payment 2494 2495 # count withholding taxes: 2496 elif "_TAX" in item["operationType"]: 2497 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2498 customStat["allTaxes"][item["payment"]["currency"]] += payment 2499 2500 else: 2501 customStat["allTaxes"][item["payment"]["currency"]] = payment 2502 2503 else: 2504 continue 2505 2506 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2507 2508 # --- view "Actions" lines: 2509 info.extend([ 2510 "| Report sections | | | | |\n", 2511 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2512 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2513 "| | Buy: {:<22} | {:<28} | | |\n".format( 2514 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2515 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2516 ), 2517 "| | Sell: {:<21} | {:<28} | | |\n".format( 2518 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2519 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2520 ), 2521 ]) 2522 2523 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2524 for key in opsKeys: 2525 if key == "rub": 2526 continue 2527 2528 info.extend([ 2529 "| | | {:<28} | | |\n".format( 2530 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2531 ), 2532 "| | | {:<28} | | |\n".format( 2533 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2534 ), 2535 ]) 2536 2537 info.append(splitLine1) 2538 2539 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2540 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2541 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2542 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2543 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2544 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2545 ) 2546 2547 # --- view "Payments" lines: 2548 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2549 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2550 2551 for key in paymentsKeys: 2552 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2553 2554 info.append(splitLine1) 2555 2556 # --- view "Commissions and taxes" lines: 2557 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2558 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2559 2560 for key in comKeys: 2561 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2562 2563 info.append(splitLine1) 2564 2565 info.extend([ 2566 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2567 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2568 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2569 ]) 2570 2571 else: 2572 info.append("Broker returned no operations during this period\n") 2573 2574 # --- view "Operations" section: 2575 for item in ops: 2576 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2577 continue 2578 2579 else: 2580 self.figi = item["figi"] if item["figi"] else "" 2581 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2582 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2583 2584 # group of deals during one day: 2585 if nextDay and item["date"].split("T")[0] != nextDay: 2586 info.append(splitLine2) 2587 nextDay = "" 2588 2589 else: 2590 nextDay = item["date"].split("T")[0] # saving current day for splitting 2591 2592 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2593 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2594 self.figi if self.figi else "—", 2595 instrument["ticker"] if instrument else "—", 2596 instrument["type"] if instrument else "—", 2597 item["quantity"] if int(item["quantity"]) > 0 else "—", 2598 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2599 TKS_OPERATION_STATES[item["state"]], 2600 TKS_OPERATION_TYPES[item["operationType"]], 2601 )) 2602 2603 infoText = "".join(info) 2604 2605 if show: 2606 if self.moreDebug: 2607 uLogger.debug("Records about history of a client's operations successfully received") 2608 2609 uLogger.info(infoText) 2610 2611 if self.reportFile: 2612 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2613 fH.write(infoText) 2614 2615 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2616 2617 return ops, customStat
Returns history operations between two given dates for current accountId.
If reportFile string is not empty then also save human-readable report.
Shows some statistical data of closed positions.
Parameters
- start: see docstring in
GetDatesAsString()method - end: see docstring in
GetDatesAsString()method - show: if
Truethen also prints all records to the console. - showCancelled: if
Falsethen remove information about cancelled operations from the deals report.
Returns
original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2619 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2620 """ 2621 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2622 2623 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2624 Warning! Broker server used ISO UTC time by default. 2625 2626 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2627 Also, `historyFile` used to update history with `onlyMissing` parameter. 2628 2629 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2630 2631 :param start: see docstring in `GetDatesAsString()` method. 2632 :param end: see docstring in `GetDatesAsString()` method. 2633 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2634 `"hour"`, `"day"`. Default: `"hour"`. 2635 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2636 False by default. Warning! History appends only from last candle to current time 2637 with always update last candle! 2638 :param csvSep: separator if csv-file is used, `,` by default. 2639 :param show: if `True` then also prints Pandas DataFrame to the console. 2640 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2641 `["date", "time", "open", "high", "low", "close", "volume"]`. 2642 """ 2643 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2644 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2645 history = None # empty pandas object for history 2646 2647 if interval not in TKS_CANDLE_INTERVALS.keys(): 2648 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2649 raise Exception("Incorrect value") 2650 2651 if not (self.ticker or self.figi): 2652 uLogger.error("Ticker or FIGI must be defined!") 2653 raise Exception("Ticker or FIGI required") 2654 2655 if self.ticker and not self.figi: 2656 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2657 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2658 2659 if self.figi and not self.ticker: 2660 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2661 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2662 2663 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2664 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2665 if interval.lower() != "day": 2666 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2667 2668 delta = dtEnd - dtStart # current UTC time minus last time in file 2669 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2670 2671 # calculate history length in candles: 2672 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2673 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2674 length += 1 # to avoid fraction time 2675 2676 # calculate data blocks count: 2677 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2678 2679 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2680 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2681 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2682 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2683 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2684 2685 tempOld = None # pandas object for old history, if --only-missing key present 2686 lastTime = None # datetime object of last old candle in file 2687 2688 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2689 uLogger.debug("--only-missing key present, add only last missing candles...") 2690 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2691 2692 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2693 2694 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2695 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2696 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2697 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2698 2699 # get last datetime object from last string in file or minus 1 delta if file is empty: 2700 if len(tempOld) > 0: 2701 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2702 2703 else: 2704 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2705 2706 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2707 2708 responseJSONs = [] # raw history blocks of data 2709 2710 blockEnd = dtEnd 2711 for item in range(blocks): 2712 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2713 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2714 2715 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2716 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2717 )) 2718 2719 if blockStart == blockEnd: 2720 uLogger.debug("Skipped this zero-length block...") 2721 2722 else: 2723 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2724 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2725 self.body = str({ 2726 "figi": self.figi, 2727 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2728 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2729 "interval": TKS_CANDLE_INTERVALS[interval][0] 2730 }) 2731 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2732 2733 if "code" in responseJSON.keys(): 2734 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2735 2736 else: 2737 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2738 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2739 2740 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2741 2742 blockEnd = blockStart 2743 2744 printCount = len(responseJSONs) # candles to show in console 2745 if responseJSONs: 2746 tempHistory = pd.DataFrame( 2747 data={ 2748 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2749 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2750 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2751 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2752 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2753 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2754 "volume": [int(item["volume"]) for item in responseJSONs], 2755 }, 2756 index=range(len(responseJSONs)), 2757 columns=["date", "time", "open", "high", "low", "close", "volume"], 2758 ) 2759 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2760 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2761 2762 # append only newest candles to old history if --only-missing key present: 2763 if onlyMissing and tempOld is not None and lastTime is not None: 2764 index = 0 # find start index in tempHistory data: 2765 2766 for i, item in tempHistory.iterrows(): 2767 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2768 2769 if curTime == lastTime: 2770 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2771 index = i 2772 printCount = index + 1 2773 break 2774 2775 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2776 2777 else: 2778 history = tempHistory # if no `--only-missing` key then load full data from server 2779 2780 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2781 2782 if history is not None and not history.empty: 2783 if show: 2784 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2785 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2786 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2787 )) 2788 2789 else: 2790 uLogger.warning("Received an empty candles history!") 2791 2792 if self.historyFile is not None: 2793 if history is not None and not history.empty: 2794 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2795 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2796 2797 else: 2798 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2799 2800 else: 2801 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2802 2803 return history
This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).
History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01.
Warning! Broker server used ISO UTC time by default.
If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame.
Also, historyFile used to update history with onlyMissing parameter.
See also: LoadHistory() and ShowHistoryChart() methods.
Parameters
- start: see docstring in
GetDatesAsString()method. - end: see docstring in
GetDatesAsString()method. - interval: this is a candle interval. Current available values are
"1min","5min","15min","hour","day". Default:"hour". - onlyMissing: if
Truethen add only last missing candles, do not request all history length fromstart. False by default. Warning! History appends only from last candle to current time with always update last candle! - csvSep: separator if csv-file is used,
,by default. - show: if
Truethen also prints Pandas DataFrame to the console.
Returns
Pandas DataFrame with prices history. Headers of columns are defined by default:
["date", "time", "open", "high", "low", "close", "volume"].
2805 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2806 """ 2807 Load candles history from csv-file and return Pandas DataFrame object. 2808 2809 See also: `History()` and `ShowHistoryChart()` methods. 2810 2811 :param filePath: path to csv-file to open. 2812 """ 2813 loadedHistory = None # init candles data object 2814 2815 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2816 2817 if os.path.exists(filePath): 2818 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2819 2820 tfStr = self.priceModel.FormattedDelta( 2821 self.priceModel.timeframe, 2822 "{days} days {hours}h {minutes}m {seconds}s", 2823 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2824 self.priceModel.timeframe, 2825 "{hours}h {minutes}m {seconds}s", 2826 ) 2827 2828 if loadedHistory is not None and not loadedHistory.empty: 2829 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2830 len(loadedHistory), 2831 tfStr, 2832 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2833 ) 2834 2835 else: 2836 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2837 2838 else: 2839 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2840 2841 return loadedHistory
Load candles history from csv-file and return Pandas DataFrame object.
See also: History() and ShowHistoryChart() methods.
Parameters
- filePath: path to csv-file to open.
2843 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2844 """ 2845 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2846 2847 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2848 Default: `index.html` (both for interact and non-interact candlesticks chart). 2849 2850 See also: `History()` and `LoadHistory()` methods. 2851 2852 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2853 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2854 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2855 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2856 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2857 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2858 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2859 """ 2860 if isinstance(candles, str): 2861 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2862 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2863 2864 elif isinstance(candles, pd.DataFrame): 2865 self.priceModel.prices = candles # set candles chain from variable 2866 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2867 2868 if "datetime" not in candles.columns: 2869 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2870 2871 else: 2872 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2873 raise Exception("Incorrect value") 2874 2875 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2876 2877 if interact: 2878 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2879 2880 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2881 2882 else: 2883 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2884 2885 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2886 2887 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart.
Default: index.html (both for interact and non-interact candlesticks chart).
See also: History() and LoadHistory() methods.
Parameters
- candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
- interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters If False then chain of candlesticks will render as not interactive Google Candlestick chart. See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
- openInBrowser: if True then immediately open chart in default browser, otherwise only path to
html-file prints to console. False by default, to avoid issues with
permissions deniedto html-file.
2889 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2890 """ 2891 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2892 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2893 2894 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2895 2896 :param operation: string "Buy" or "Sell". 2897 :param lots: volume, integer count of lots >= 1. 2898 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2899 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2900 :param expDate: string "Undefined" by default or local date in future, 2901 it is a string with format `%Y-%m-%d %H:%M:%S`. 2902 :return: JSON with response from broker server. 2903 """ 2904 if self.accountId is None or not self.accountId: 2905 uLogger.error("Variable `accountId` must be defined for using this method!") 2906 raise Exception("Account ID required") 2907 2908 if operation is None or not operation or operation not in ("Buy", "Sell"): 2909 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2910 raise Exception("Incorrect value") 2911 2912 if lots is None or lots < 1: 2913 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2914 lots = 1 2915 2916 if tp is None or tp < 0: 2917 tp = 0 2918 2919 if sl is None or sl < 0: 2920 sl = 0 2921 2922 if expDate is None or not expDate: 2923 expDate = "Undefined" 2924 2925 if not (self.ticker or self.figi): 2926 uLogger.error("Ticker or FIGI must be defined!") 2927 raise Exception("Ticker or FIGI required") 2928 2929 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 2930 self.ticker = instrument["ticker"] 2931 self.figi = instrument["figi"] 2932 2933 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2934 2935 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2936 self.body = str({ 2937 "figi": self.figi, 2938 "quantity": str(lots), 2939 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2940 "accountId": str(self.accountId), 2941 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2942 }) 2943 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 2944 2945 if "orderId" in response.keys(): 2946 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2947 operation, response["orderId"], 2948 self.ticker, self.figi, lots, 2949 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2950 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2951 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2952 )) 2953 2954 if tp > 0: 2955 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2956 2957 if sl > 0: 2958 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2959 2960 else: 2961 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.") 2962 2963 return response
Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().
Parameters
- operation: string "Buy" or "Sell".
- lots: volume, integer count of lots >= 1.
- tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter
targetPriceinself.Order(). - sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter
targetPriceinself.Order(). - expDate: string "Undefined" by default or local date in future,
it is a string with format
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2965 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2966 """ 2967 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2968 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2969 2970 See also: `Order()` and `Trade()` docstrings. 2971 2972 :param lots: volume, integer count of lots >= 1. 2973 :param tp: float > 0, take profit price of stop-order. 2974 :param sl: float > 0, stop loss price of stop-order. 2975 :param expDate: it's a local date in future. 2976 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2977 :return: JSON with response from broker server. 2978 """ 2979 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2981 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2982 """ 2983 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2984 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2985 2986 See also: `Order()` and `Trade()` docstrings. 2987 2988 :param lots: volume, integer count of lots >= 1. 2989 :param tp: float > 0, take profit price of stop-order. 2990 :param sl: float > 0, stop loss price of stop-order. 2991 :param expDate: it's a local date in the future. 2992 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2993 :return: JSON with response from broker server. 2994 """ 2995 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in the future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2997 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 2998 """ 2999 Close position of given instruments. 3000 3001 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3002 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3003 This avoids unnecessary downloading data from the server. 3004 """ 3005 if instruments is None or not instruments: 3006 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3007 raise Exception("Ticker or FIGI required") 3008 3009 if isinstance(instruments, str): 3010 instruments = [instruments] 3011 3012 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3013 if uniqueInstruments: 3014 if portfolio is None or not portfolio: 3015 portfolio = self.Overview(show=False) 3016 3017 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3018 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3019 3020 for self.figi in uniqueInstruments: 3021 if self.figi not in allOpened: 3022 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi)) 3023 continue 3024 3025 # search open trade info about instrument by ticker: 3026 instrument = {} 3027 for iType in TKS_INSTRUMENTS: 3028 if instrument: 3029 break 3030 3031 for item in portfolio["stat"][iType]: 3032 if item["figi"] == self.figi: 3033 instrument = item 3034 break 3035 3036 if instrument: 3037 self.ticker = instrument["ticker"] 3038 self.figi = instrument["figi"] 3039 3040 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3041 self.ticker, 3042 self.figi, 3043 int(instrument["volume"]), 3044 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3045 )) 3046 3047 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3048 3049 if tradeLots > 0: 3050 if instrument["blocked"] > 0: 3051 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3052 instrument["blocked"], 3053 self.ticker, 3054 tradeLots, 3055 )) 3056 3057 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3058 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3059 3060 else: 3061 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
Close position of given instruments.
Parameters
- instruments: list of instruments defined by tickers or FIGIs that must be closed.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3063 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3064 """ 3065 Close all positions of given instruments with defined type. 3066 3067 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3068 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3069 This avoids unnecessary downloading data from the server. 3070 """ 3071 if iType not in TKS_INSTRUMENTS: 3072 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3073 3074 else: 3075 if portfolio is None or not portfolio: 3076 portfolio = self.Overview(show=False) 3077 3078 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3079 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3080 3081 if tickers and portfolio: 3082 self.CloseTrades(tickers, portfolio) 3083 3084 else: 3085 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
Close all positions of given instruments with defined type.
Parameters
- iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3087 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3088 """ 3089 Universal method to create market or limit orders with all available parameters for current `accountId`. 3090 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3091 3092 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3093 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3094 3095 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3096 then broker immediately open market order as you can do simple --buy or --sell operations! 3097 3098 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3099 When current price will go up or down to target price value then broker opens a limit order. 3100 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3101 3102 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3103 3104 :param operation: string "Buy" or "Sell". 3105 :param orderType: string "Limit" or "Stop". 3106 :param lots: volume, integer count of lots >= 1. 3107 :param targetPrice: target price > 0. This is open trade price for limit order. 3108 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3109 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3110 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3111 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3112 Stop loss order always executed by market price. 3113 :param expDate: string "Undefined" by default or local date in future. 3114 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3115 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3116 A limit order has no expiration date, it lasts until the end of the trading day. 3117 :return: JSON with response from broker server. 3118 """ 3119 if self.accountId is None or not self.accountId: 3120 uLogger.error("Variable `accountId` must be defined for using this method!") 3121 raise Exception("Account ID required") 3122 3123 if operation is None or not operation or operation not in ("Buy", "Sell"): 3124 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3125 raise Exception("Incorrect value") 3126 3127 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3128 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3129 raise Exception("Incorrect value") 3130 3131 if lots is None or lots < 1: 3132 uLogger.error("You must define trade volume > 0: integer count of lots!") 3133 raise Exception("Incorrect value") 3134 3135 if targetPrice is None or targetPrice <= 0: 3136 uLogger.error("Target price for limit-order must be greater than 0!") 3137 raise Exception("Incorrect value") 3138 3139 if limitPrice is None or limitPrice <= 0: 3140 limitPrice = targetPrice 3141 3142 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3143 stopType = "Limit" 3144 3145 if expDate is None or not expDate: 3146 expDate = "Undefined" 3147 3148 if not (self.ticker or self.figi): 3149 uLogger.error("Tocker or FIGI must be defined!") 3150 raise Exception("Ticker or FIGI required") 3151 3152 response = {} 3153 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 3154 self.ticker = instrument["ticker"] 3155 self.figi = instrument["figi"] 3156 3157 if orderType == "Limit": 3158 uLogger.debug( 3159 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3160 self.ticker, self.figi, 3161 operation, lots, targetPrice, instrument["currency"], 3162 )) 3163 3164 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3165 self.body = str({ 3166 "figi": self.figi, 3167 "quantity": str(lots), 3168 "price": FloatToNano(targetPrice), 3169 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3170 "accountId": str(self.accountId), 3171 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3172 }) 3173 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3174 3175 if "orderId" in response.keys(): 3176 uLogger.info( 3177 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3178 response["orderId"], 3179 self.ticker, self.figi, 3180 operation, lots, targetPrice, instrument["currency"], 3181 )) 3182 3183 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3184 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3185 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3186 targetPrice, instrument["currency"], 3187 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3188 )) 3189 3190 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3191 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3192 targetPrice, instrument["currency"], 3193 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3194 )) 3195 3196 else: 3197 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3198 3199 if orderType == "Stop": 3200 uLogger.debug( 3201 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3202 self.ticker, self.figi, 3203 operation, lots, 3204 targetPrice, instrument["currency"], 3205 limitPrice, instrument["currency"], 3206 stopType, expDate, 3207 )) 3208 3209 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3210 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3211 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3212 3213 body = { 3214 "figi": self.figi, 3215 "quantity": str(lots), 3216 "price": FloatToNano(limitPrice), 3217 "stopPrice": FloatToNano(targetPrice), 3218 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3219 "accountId": str(self.accountId), 3220 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3221 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3222 } 3223 3224 if expDateUTC: 3225 body["expireDate"] = expDateUTC 3226 3227 self.body = str(body) 3228 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3229 3230 if "stopOrderId" in response.keys(): 3231 uLogger.info( 3232 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3233 response["stopOrderId"], 3234 self.ticker, self.figi, 3235 operation, lots, 3236 targetPrice, instrument["currency"], 3237 limitPrice, instrument["currency"], 3238 TKS_STOP_ORDER_TYPES[stopOrderType], 3239 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3240 )) 3241 3242 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3243 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3244 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3245 targetPrice, instrument["currency"], 3246 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3247 )) 3248 3249 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3250 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3251 targetPrice, instrument["currency"], 3252 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3253 )) 3254 3255 else: 3256 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3257 3258 return response
Universal method to create market or limit orders with all available parameters for current accountId.
See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().
If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!
If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
Only one attempt and no retry for opens order. If network issue occurred you can create new request.
Parameters
- operation: string "Buy" or "Sell".
- orderType: string "Limit" or "Stop".
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
- limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
- stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns
JSON with response from broker server.
3260 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3261 """ 3262 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3263 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3264 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3265 See also: `Order()` docstring. 3266 3267 :param lots: volume, integer count of lots >= 1. 3268 :param targetPrice: target price > 0. This is open trade price for limit order. 3269 :return: JSON with response from broker server. 3270 """ 3271 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Buy limit-order (below current price). You must specify only 2 parameters:
lots and target price to open buy limit-order. If you try to create buy limit-order above current price then
broker immediately open Buy market order, such as if you do simple --buy operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3273 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3274 """ 3275 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3276 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3277 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3278 target price value then broker opens a limit order. See also: `Order()` docstring. 3279 3280 :param lots: volume, integer count of lots >= 1. 3281 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3282 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3283 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3284 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3285 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3286 :param expDate: string "Undefined" by default or local date in future. 3287 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3288 This date is converting to UTC format for server. 3289 :return: JSON with response from broker server. 3290 """ 3291 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order.
In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for buy stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3293 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3294 """ 3295 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3296 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3297 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3298 See also: `Order()` docstring. 3299 3300 :param lots: volume, integer count of lots >= 1. 3301 :param targetPrice: target price > 0. This is open trade price for limit order. 3302 :return: JSON with response from broker server. 3303 """ 3304 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Sell limit-order (above current price). You must specify only 2 parameters:
lots and target price to open sell limit-order. If you try to create sell limit-order below current price then
broker immediately open Sell market order, such as if you do simple --sell operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3306 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3307 """ 3308 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3309 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3310 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3311 target price value then broker opens a limit order. See also: `Order()` docstring. 3312 3313 :param lots: volume, integer count of lots >= 1. 3314 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3315 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3316 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3317 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3318 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3319 :param expDate: string "Undefined" by default or local date in future. 3320 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3321 This date is converting to UTC format for server. 3322 :return: JSON with response from broker server. 3323 """ 3324 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order.
In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for sell stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3326 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3327 """ 3328 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3329 3330 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3331 :param allOrdersIDs: pre-received lists of all active pending orders. 3332 This avoids unnecessary downloading data from the server. 3333 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3334 """ 3335 if self.accountId is None or not self.accountId: 3336 uLogger.error("Variable `accountId` must be defined for using this method!") 3337 raise Exception("Account ID required") 3338 3339 if orderIDs: 3340 if allOrdersIDs is None or not allOrdersIDs: 3341 rawOrders = self.RequestPendingOrders() 3342 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3343 3344 if allStopOrdersIDs is None or not allStopOrdersIDs: 3345 rawStopOrders = self.RequestStopOrders() 3346 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3347 3348 for orderID in orderIDs: 3349 idInPendingOrders = orderID in allOrdersIDs 3350 idInStopOrders = orderID in allStopOrdersIDs 3351 3352 if not (idInPendingOrders or idInStopOrders): 3353 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3354 continue 3355 3356 else: 3357 if idInPendingOrders: 3358 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3359 3360 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3361 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3362 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3363 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3364 3365 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3366 if self.moreDebug: 3367 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3368 3369 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3370 3371 else: 3372 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3373 3374 elif idInStopOrders: 3375 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3376 3377 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3378 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3379 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3380 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3381 3382 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3383 if self.moreDebug: 3384 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3385 3386 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3387 3388 else: 3389 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3390 3391 else: 3392 continue
Cancel order or list of orders by its orderId or stopOrderId for current accountId.
Parameters
- orderIDs: list of integers with
orderIdorstopOrderId. - allOrdersIDs: pre-received lists of all active pending orders. This avoids unnecessary downloading data from the server.
- allStopOrdersIDs: pre-received lists of all active stop orders.
3394 def CloseAllOrders(self) -> None: 3395 """ 3396 Gets a list of open pending and stop orders and cancel it all. 3397 """ 3398 rawOrders = self.RequestPendingOrders() 3399 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3400 lenOrders = len(allOrdersIDs) 3401 3402 rawStopOrders = self.RequestStopOrders() 3403 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3404 lenSOrders = len(allStopOrdersIDs) 3405 3406 if lenOrders > 0 or lenSOrders > 0: 3407 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3408 3409 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3410 3411 else: 3412 uLogger.info("Orders not found, nothing to cancel.")
Gets a list of open pending and stop orders and cancel it all.
3414 def CloseAll(self, *args) -> None: 3415 """ 3416 Close all available (not blocked) opened trades and orders. 3417 3418 Also, you can select one or more keywords case-insensitive: 3419 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3420 3421 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3422 """ 3423 overview = self.Overview(show=False) # get all open trades info 3424 3425 if len(args) == 0: 3426 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3427 self.CloseAllOrders() # close all pending and stop orders 3428 3429 for iType in TKS_INSTRUMENTS: 3430 if iType != "Currencies": 3431 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3432 3433 else: 3434 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3435 lowerArgs = [x.lower() for x in args] 3436 3437 if "orders" in lowerArgs: 3438 self.CloseAllOrders() # close all pending and stop orders 3439 3440 for iType in TKS_INSTRUMENTS: 3441 if iType.lower() in lowerArgs and iType != "Currencies": 3442 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies
Close all available (not blocked) opened trades and orders.
Also, you can select one or more keywords case-insensitive:
orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.
Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.
3444 @staticmethod 3445 def ParseOrderParameters(operation, **inputParameters): 3446 """ 3447 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3448 3449 :param operation: string "Buy" or "Sell". 3450 :param inputParameters: this is dict of strings that looks like this 3451 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3452 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3453 "prices" key: one or more prices to open limit-orders 3454 Counts of values in lots and prices lists must be equals! 3455 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3456 """ 3457 # TODO: update order grid work with api v2 3458 pass 3459 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3460 # 3461 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3462 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3463 # raise Exception("Incorrect value") 3464 # 3465 # if "l" in inputParameters.keys(): 3466 # inputParameters["lots"] = inputParameters.pop("l") 3467 # 3468 # if "p" in inputParameters.keys(): 3469 # inputParameters["prices"] = inputParameters.pop("p") 3470 # 3471 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3472 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3473 # raise Exception("Incorrect value") 3474 # 3475 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3476 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3477 # 3478 # if len(lots) != len(prices): 3479 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3480 # raise Exception("Incorrect value") 3481 # 3482 # uLogger.debug("Extracted parameters for orders:") 3483 # uLogger.debug("lots = {}".format(lots)) 3484 # uLogger.debug("prices = {}".format(prices)) 3485 # 3486 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3487 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3488 # uLogger.debug("Order parameters: {}".format(result)) 3489 # 3490 # return result
Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
Parameters
- operation: string "Buy" or "Sell".
- inputParameters: this is dict of strings that looks like this
{"lots": "L_int,...", "prices": "P_float,..."}where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns
list of dictionaries with all lots and prices to open orders that looks like this
[{"lot": lots_1, "price": price_1}, {...}, ...]
3492 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3493 """ 3494 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3495 3496 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3497 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3498 """ 3499 result = False 3500 msg = "Instrument not defined!" 3501 3502 if portfolio is None or not portfolio: 3503 portfolio = self.Overview(show=False) 3504 3505 if self.ticker: 3506 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3507 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3508 3509 for iType in TKS_INSTRUMENTS: 3510 for instrument in portfolio["stat"][iType]: 3511 if instrument["ticker"] == self.ticker: 3512 result = True 3513 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3514 break 3515 3516 elif self.figi: 3517 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3518 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3519 3520 for iType in TKS_INSTRUMENTS: 3521 for instrument in portfolio["stat"][iType]: 3522 if instrument["figi"] == self.figi: 3523 result = True 3524 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3525 break 3526 3527 else: 3528 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3529 3530 uLogger.debug(msg) 3531 3532 return result
Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif portfolio contains open position with given instrument,Falseotherwise.
3534 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3535 """ 3536 Returns instrument from the user's portfolio if it presents there. 3537 Instrument must be defined by `ticker` (highly priority) or `figi`. 3538 3539 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3540 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3541 """ 3542 result = None 3543 msg = "Instrument not defined!" 3544 3545 if portfolio is None or not portfolio: 3546 portfolio = self.Overview(show=False) 3547 3548 if self.ticker: 3549 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self.ticker)) 3550 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3551 3552 for iType in TKS_INSTRUMENTS: 3553 for instrument in portfolio["stat"][iType]: 3554 if instrument["ticker"] == self.ticker: 3555 result = instrument 3556 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3557 break 3558 3559 elif self.figi: 3560 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3561 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3562 3563 for iType in TKS_INSTRUMENTS: 3564 for instrument in portfolio["stat"][iType]: 3565 if instrument["figi"] == self.figi: 3566 result = instrument 3567 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3568 break 3569 3570 else: 3571 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3572 3573 uLogger.debug(msg) 3574 3575 return result
Returns instrument from the user's portfolio if it presents there.
Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
dict with instrument if portfolio contains open position with this instrument,
Noneotherwise.
3577 def RequestLimits(self) -> dict: 3578 """ 3579 Method for obtaining the available funds for withdrawal for current `accountId`. 3580 3581 See also: 3582 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3583 - `OverviewLimits()` method 3584 3585 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3586 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3587 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3588 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3589 """ 3590 if self.accountId is None or not self.accountId: 3591 uLogger.error("Variable `accountId` must be defined for using this method!") 3592 raise Exception("Account ID required") 3593 3594 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3595 3596 self.body = str({"accountId": self.accountId}) 3597 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3598 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3599 3600 if self.moreDebug: 3601 uLogger.debug("Records about available funds for withdrawal successfully received") 3602 3603 return rawLimits
Method for obtaining the available funds for withdrawal for current accountId.
See also:
- REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
OverviewLimits()method
Returns
dict with raw data from server that contains free funds for withdrawal. Example of dict:
{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Heremoneyis an array of portfolio currency positions,blockedis an array of blocked currency positions of the portfolio andblockedGuaranteeis locked money under collateral for futures.
3605 def OverviewLimits(self, show: bool = False) -> dict: 3606 """ 3607 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3608 3609 See also: `RequestLimits()`. 3610 3611 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3612 :return: dict with raw parsed data from server and some calculated statistics about it. 3613 """ 3614 if self.accountId is None or not self.accountId: 3615 uLogger.error("Variable `accountId` must be defined for using this method!") 3616 raise Exception("Account ID required") 3617 3618 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3619 3620 view = { 3621 "rawLimits": rawLimits, 3622 "limits": { # parsed data for every currency: 3623 "money": { # this is an array of portfolio currency positions 3624 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3625 }, 3626 "blocked": { # this is an array of blocked currency 3627 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3628 }, 3629 "blockedGuarantee": { # this is locked money under collateral for futures 3630 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3631 }, 3632 }, 3633 } 3634 3635 # --- Prepare text table with limits in human-readable format: 3636 if show: 3637 info = [ 3638 "# Withdrawal limits\n\n", 3639 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3640 "* **Account ID:** [{}]\n".format(self.accountId), 3641 ] 3642 3643 if view["limits"]["money"]: 3644 info.extend([ 3645 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3646 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3647 ]) 3648 3649 else: 3650 info.append("\nNo withdrawal limits\n") 3651 3652 for curr in view["limits"]["money"].keys(): 3653 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3654 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3655 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3656 3657 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3658 "[{}]".format(curr), 3659 "{:.2f}".format(view["limits"]["money"][curr]), 3660 "{:.2f}".format(availableMoney), 3661 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3662 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3663 ) 3664 3665 if curr == "rub": 3666 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3667 3668 else: 3669 info.append(infoStr) 3670 3671 infoText = "".join(info) 3672 3673 uLogger.info(infoText) 3674 3675 if self.withdrawalLimitsFile: 3676 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3677 fH.write(infoText) 3678 3679 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3680 3681 return view
Method for parsing and show table with available funds for withdrawal for current accountId.
See also: RequestLimits().
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print withdrawal limits to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
3683 def RequestAccounts(self) -> dict: 3684 """ 3685 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3686 3687 See also: 3688 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3689 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3690 - `OverviewUserInfo()` method 3691 3692 :return: dict with raw data from server that contains accounts info. Example of dict: 3693 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3694 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3695 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3696 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3697 """ 3698 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3699 3700 self.body = str({}) 3701 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3702 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3703 3704 if self.moreDebug: 3705 uLogger.debug("Records about available accounts successfully received") 3706 3707 return rawAccounts
Method for requesting all brokerage accounts (accountIds) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
- What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
OverviewUserInfo()method
Returns
dict with raw data from server that contains accounts info. Example of dict:
{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. IfclosedDate="1970-01-01T00:00:00Z"it means that account is active now.
3709 def RequestUserInfo(self) -> dict: 3710 """ 3711 Method for requesting common user's information. 3712 3713 See also: 3714 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3715 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3716 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3717 - `OverviewUserInfo()` method 3718 3719 :return: dict with raw data from server that contains user's information. Example of dict: 3720 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3721 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3722 """ 3723 uLogger.debug("Requesting common user's information. Wait, please...") 3724 3725 self.body = str({}) 3726 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3727 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3728 3729 if self.moreDebug: 3730 uLogger.debug("Records about current user successfully received") 3731 3732 return rawUserInfo
Method for requesting common user's information.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
- What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
- What does
qualified_for_work_withfield mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with OverviewUserInfo()method
Returns
dict with raw data from server that contains user's information. Example of dict:
{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.
3734 def RequestMarginStatus(self, accountId: str = None) -> dict: 3735 """ 3736 Method for requesting margin calculation for defined account ID. 3737 3738 See also: 3739 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3740 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3741 - `OverviewUserInfo()` method 3742 3743 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3744 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3745 Example of responses: 3746 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3747 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3748 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3749 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3750 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3751 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3752 """ 3753 if accountId is None or not accountId: 3754 if self.accountId is None or not self.accountId: 3755 uLogger.error("Variable `accountId` must be defined for using this method!") 3756 raise Exception("Account ID required") 3757 3758 else: 3759 accountId = self.accountId # use `self.accountId` (main ID) by default 3760 3761 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3762 3763 self.body = str({"accountId": accountId}) 3764 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3765 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3766 3767 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3768 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3769 rawMargin = {} 3770 3771 else: 3772 if self.moreDebug: 3773 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3774 3775 return rawMargin
Method for requesting margin calculation for defined account ID.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
- What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
OverviewUserInfo()method
Parameters
- accountId: string with numeric account ID. If
None, then used class fieldaccountId.
Returns
dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400:
{"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns:{}. status code 200:{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.
3777 def RequestTariffLimits(self) -> dict: 3778 """ 3779 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3780 3781 See also: 3782 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3783 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3784 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3785 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3786 - `OverviewUserInfo()` method 3787 3788 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3789 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3790 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3791 """ 3792 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3793 3794 self.body = str({}) 3795 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3796 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3797 3798 if self.moreDebug: 3799 uLogger.debug("Records with limits of current tariff successfully received") 3800 3801 return rawTariffLimits
Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
- What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
- Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
- Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
OverviewUserInfo()method
Returns
dict with raw data from server that contains limits of current tariff. Example of dict:
{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.
3803 def RequestBondCoupons(self, iJSON: dict) -> dict: 3804 """ 3805 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3806 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3807 All dates are in UTC timezone. 3808 3809 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3810 Documentation: 3811 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3812 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3813 3814 See also: `ExtendBondsData()`. 3815 3816 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3817 If raw iJSON is not data of bond then server returns an error [400] with message: 3818 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3819 :return: dictionary with bond payment calendar. Response example 3820 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3821 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3822 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3823 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3824 """ 3825 if iJSON["figi"] is None or not iJSON["figi"]: 3826 uLogger.error("FIGI must be defined for using this method!") 3827 raise Exception("FIGI required") 3828 3829 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3830 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3831 3832 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3833 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3834 self.figi, 3835 startDate, 3836 endDate, 3837 )) 3838 3839 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3840 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3841 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 3842 3843 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3844 uLogger.warning("Instrument type is not bond!") 3845 3846 else: 3847 if self.moreDebug: 3848 uLogger.debug("Records about bond payment calendar successfully received") 3849 3850 return calendar
Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z".
All dates are in UTC timezone.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:
- request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
- response: https://tinkoff.github.io/investAPI/instruments/#coupon
See also: ExtendBondsData().
Parameters
- iJSON: raw json data of a bond from broker server, example
iJSON = self.iList["Bonds"][self.ticker]If raw iJSON is not data of bond then server returns an error [400] with message:{"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns
dictionary with bond payment calendar. Response example
{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}
3852 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3853 """ 3854 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3855 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 3856 coupon yields, current yields and some statistics etc. 3857 3858 WARNING! This is too long operation if a lot of bonds requested from broker server. 3859 3860 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3861 3862 :param instruments: list of strings with tickers or FIGIs. 3863 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3864 for further used by data scientists or stock analytics. 3865 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 3866 In XLSX-file and Pandas DataFrame fields mean: 3867 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3868 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3869 """ 3870 if instruments is None or not instruments: 3871 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3872 raise Exception("Ticker or FIGI required") 3873 3874 if isinstance(instruments, str): 3875 instruments = [instruments] 3876 3877 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3878 3879 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3880 3881 iCount = len(uniqueInstruments) 3882 tooLong = iCount >= 20 3883 if tooLong: 3884 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3885 3886 bonds = None 3887 for i, self.figi in enumerate(uniqueInstruments): 3888 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3889 3890 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3891 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3892 rawBond = self.SearchByFIGI(requestPrice=True) 3893 3894 # Widen raw data with UTC current time (iData["actualDateTime"]): 3895 actualDate = datetime.now(tzutc()) 3896 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3897 3898 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3899 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3900 3901 # Replace some values with human-readable: 3902 iData["nominalCurrency"] = iData["nominal"]["currency"] 3903 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3904 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3905 iData["aciCurrency"] = iData["aciValue"]["currency"] 3906 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3907 iData["issueSize"] = int(iData["issueSize"]) 3908 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3909 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3910 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3911 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3912 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3913 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3914 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3915 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3916 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3917 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3918 3919 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3920 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3921 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3922 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3923 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3924 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3925 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3926 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3927 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3928 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3929 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3930 3931 # Widen raw data with calendar data from `rawCalendar` values: 3932 calendarData = [] 3933 if "events" in iData["rawCalendar"].keys(): 3934 for item in iData["rawCalendar"]["events"]: 3935 calendarData.append({ 3936 "couponDate": item["couponDate"], 3937 "couponNumber": int(item["couponNumber"]), 3938 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3939 "payCurrency": item["payOneBond"]["currency"], 3940 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3941 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3942 "couponStartDate": item["couponStartDate"], 3943 "couponEndDate": item["couponEndDate"], 3944 "couponPeriod": item["couponPeriod"], 3945 }) 3946 3947 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3948 if "maturityDate" not in iData.keys(): 3949 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3950 3951 # Widen raw data with Coupon Rate. 3952 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3953 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3954 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3955 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3956 3957 # Widen raw data with Yield to Maturity (YTM) on current date. 3958 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3959 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3960 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3961 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3962 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3963 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3964 3965 iData["calendar"] = calendarData # adds calendar at the end 3966 3967 # Remove not used data: 3968 iData.pop("uid") 3969 iData.pop("positionUid") 3970 iData.pop("currentPrice") 3971 iData.pop("rawCalendar") 3972 3973 colNames = list(iData.keys()) 3974 if bonds is None: 3975 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3976 3977 else: 3978 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 3979 3980 else: 3981 uLogger.warning("Instrument is not a bond!") 3982 3983 processed = round(100 * (i + 1) / iCount, 1) 3984 if tooLong and processed % 5 == 0: 3985 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 3986 3987 else: 3988 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 3989 3990 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 3991 3992 # Saving bonds from Pandas DataFrame to XLSX sheet: 3993 if xlsx and self.bondsXLSXFile: 3994 with pd.ExcelWriter( 3995 path=self.bondsXLSXFile, 3996 date_format=TKS_DATE_FORMAT, 3997 datetime_format=TKS_DATE_TIME_FORMAT, 3998 mode="w", 3999 ) as writer: 4000 bonds.to_excel( 4001 writer, 4002 sheet_name="Extended bonds data", 4003 index=True, 4004 encoding="UTF-8", 4005 freeze_panes=(1, 1), 4006 ) # saving as XLSX-file with freeze first row and column as headers 4007 4008 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4009 4010 return bonds
Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().
Parameters
- instruments: list of strings with tickers or FIGIs.
- xlsx: if True then also exports Pandas DataFrame to xlsx-file
bondsXLSXFile, defaultext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns
wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4012 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4013 """ 4014 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4015 4016 WARNING! This is too long operation if a lot of bonds requested from broker server. 4017 4018 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4019 4020 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4021 extended information about bonds: main info, current prices, bond payment calendar, 4022 coupon yields, current yields and some statistics etc. 4023 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4024 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4025 for further used by data scientists or stock analytics. 4026 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4027 """ 4028 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4029 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4030 4031 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4032 4033 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4034 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4035 calendar = None 4036 for bond in extBonds.iterrows(): 4037 for item in bond[1]["calendar"]: 4038 cData = { 4039 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4040 "couponDate": item["couponDate"], 4041 "figi": bond[1]["figi"], 4042 "ticker": bond[1]["ticker"], 4043 "name": bond[1]["name"], 4044 "couponNumber": item["couponNumber"], 4045 "payOneBond": item["payOneBond"], 4046 "payCurrency": item["payCurrency"], 4047 "couponType": item["couponType"], 4048 "couponPeriod": item["couponPeriod"], 4049 "fixDate": item["fixDate"], 4050 "couponStartDate": item["couponStartDate"], 4051 "couponEndDate": item["couponEndDate"], 4052 } 4053 4054 if calendar is None: 4055 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4056 4057 else: 4058 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4059 4060 if calendar is not None: 4061 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4062 4063 # Saving calendar from Pandas DataFrame to XLSX sheet: 4064 if xlsx: 4065 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4066 4067 with pd.ExcelWriter( 4068 path=xlsxCalendarFile, 4069 date_format=TKS_DATE_FORMAT, 4070 datetime_format=TKS_DATE_TIME_FORMAT, 4071 mode="w", 4072 ) as writer: 4073 humanReadable = calendar.copy(deep=True) 4074 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4075 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4076 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4077 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4078 humanReadable.columns = colNames # human-readable column names 4079 4080 humanReadable.to_excel( 4081 writer, 4082 sheet_name="Bond payments calendar", 4083 index=False, 4084 encoding="UTF-8", 4085 freeze_panes=(1, 2), 4086 ) # saving as XLSX-file with freeze first row and column as headers 4087 4088 del humanReadable # release df in memory 4089 4090 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4091 4092 return calendar
Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowBondsCalendar(), ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - xlsx: if True then also exports Pandas DataFrame to file
calendarFile+".xlsx",calendar.xlsxby default, for further used by data scientists or stock analytics.
Returns
Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4094 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4095 """ 4096 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4097 Also, creates Markdown file with calendar data, `calendar.md` by default. 4098 4099 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4100 4101 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4102 extended information about bonds: main info, current prices, bond payment calendar, 4103 coupon yields, current yields and some statistics etc. 4104 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4105 :param show: if `True` then also printing bonds payment calendar to the console, 4106 otherwise save to file `calendarFile` only. `False` by default. 4107 :return: multilines text in Markdown format with bonds payment calendar as a table. 4108 """ 4109 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4110 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4111 4112 infoText = "# Bond payments calendar\n\n" 4113 4114 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4115 4116 if not (calendar is None or calendar.empty): 4117 splitLine = "| | | | | | | | | |\n" 4118 4119 info = [ 4120 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4121 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4122 ] 4123 4124 newMonth = False 4125 notOneBond = calendar["figi"].nunique() > 1 4126 for i, bond in enumerate(calendar.iterrows()): 4127 if newMonth and notOneBond: 4128 info.append(splitLine) 4129 4130 info.append( 4131 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4132 " √" if bond[1]["paid"] else " —", 4133 bond[1]["couponDate"].split("T")[0], 4134 bond[1]["figi"], 4135 bond[1]["ticker"], 4136 bond[1]["couponNumber"], 4137 "{} {}".format( 4138 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4139 bond[1]["payCurrency"], 4140 ), 4141 bond[1]["couponType"], 4142 bond[1]["couponPeriod"], 4143 bond[1]["fixDate"].split("T")[0], 4144 ) 4145 ) 4146 4147 if i < len(calendar.values) - 1: 4148 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4149 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4150 newMonth = False if curDate.month == nextDate.month else True 4151 4152 else: 4153 newMonth = False 4154 4155 infoText += "".join(info) 4156 4157 if show: 4158 uLogger.info("{}".format(infoText)) 4159 4160 if self.calendarFile is not None: 4161 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4162 fH.write(infoText) 4163 4164 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4165 4166 else: 4167 infoText += "No data\n" 4168 4169 return infoText
Show bond payments calendar as a table. One row in input bonds dataframe contains one bond.
Also, creates Markdown file with calendar data, calendar.md by default.
See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - show: if
Truethen also printing bonds payment calendar to the console, otherwise save to filecalendarFileonly.Falseby default.
Returns
multilines text in Markdown format with bonds payment calendar as a table.
4171 def OverviewAccounts(self, show: bool = False) -> dict: 4172 """ 4173 Method for parsing and show simple table with all available user accounts. 4174 4175 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4176 4177 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4178 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4179 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4180 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4181 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4182 "closed": "—", "access": "Full access" }, ...}}` 4183 """ 4184 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4185 4186 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4187 accounts = { 4188 item["id"]: { 4189 "type": TKS_ACCOUNT_TYPES[item["type"]], 4190 "name": item["name"], 4191 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4192 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4193 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4194 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4195 } for item in rawAccounts["accounts"] 4196 } 4197 4198 # Raw and parsed data with some fields replaced in "stat" section: 4199 view = { 4200 "rawAccounts": rawAccounts, 4201 "stat": accounts, 4202 } 4203 4204 # --- Prepare simple text table with only accounts data in human-readable format: 4205 if show: 4206 info = [ 4207 "# User accounts\n\n", 4208 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4209 "| Account ID | Type | Status | Name |\n", 4210 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4211 ] 4212 4213 for account in view["stat"].keys(): 4214 info.extend([ 4215 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4216 account, 4217 view["stat"][account]["type"], 4218 view["stat"][account]["status"], 4219 view["stat"][account]["name"], 4220 ) 4221 ]) 4222 4223 infoText = "".join(info) 4224 4225 uLogger.info(infoText) 4226 4227 if self.userAccountsFile: 4228 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4229 fH.write(infoText) 4230 4231 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4232 4233 return view
Method for parsing and show simple table with all available user accounts.
See also: RequestAccounts() and OverviewUserInfo() methods.
Parameters
- show: if
Falsethen only dictionary with accounts data returns, ifTruethen also print it to log.
Returns
dict with parsed accounts data received from
RequestAccounts()method. Example of dict:view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}
4235 def OverviewUserInfo(self, show: bool = False) -> dict: 4236 """ 4237 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4238 4239 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4240 4241 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4242 :return: dict with raw parsed data from server and some calculated statistics about it. 4243 """ 4244 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4245 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4246 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4247 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4248 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4249 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4250 4251 # This is dict with parsed common user data: 4252 userInfo = { 4253 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4254 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4255 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4256 "tariff": rawUserInfo["tariff"], 4257 } 4258 4259 # This is an array of dict with parsed margin statuses for every account IDs: 4260 margins = {} 4261 for accountId in accounts.keys(): 4262 if rawMargins[accountId]: 4263 margins[accountId] = { 4264 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4265 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4266 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4267 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4268 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4269 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4270 } 4271 4272 else: 4273 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4274 4275 unary = {} # unary-connection limits 4276 for item in rawTariffLimits["unaryLimits"]: 4277 if item["limitPerMinute"] in unary.keys(): 4278 unary[item["limitPerMinute"]].extend(item["methods"]) 4279 4280 else: 4281 unary[item["limitPerMinute"]] = item["methods"] 4282 4283 stream = {} # stream-connection limits 4284 for item in rawTariffLimits["streamLimits"]: 4285 if item["limit"] in stream.keys(): 4286 stream[item["limit"]].extend(item["streams"]) 4287 4288 else: 4289 stream[item["limit"]] = item["streams"] 4290 4291 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4292 limits = { 4293 "unary": unary, 4294 "stream": stream, 4295 } 4296 4297 # Raw and parsed data as an output result: 4298 view = { 4299 "rawUserInfo": rawUserInfo, 4300 "rawAccounts": rawAccounts, 4301 "rawMargins": rawMargins, 4302 "rawTariffLimits": rawTariffLimits, 4303 "stat": { 4304 "userInfo": userInfo, 4305 "accounts": accounts, 4306 "margins": margins, 4307 "limits": limits, 4308 }, 4309 } 4310 4311 # --- Prepare text table with user information in human-readable format: 4312 if show: 4313 info = [ 4314 "# Full user information\n\n", 4315 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4316 "## Common information\n\n", 4317 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4318 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4319 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4320 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4321 "\n## User accounts\n\n", 4322 ] 4323 4324 for account in view["stat"]["accounts"].keys(): 4325 info.extend([ 4326 "### ID: [{}]\n\n".format(account), 4327 "| Parameters | Values |\n", 4328 "|----------------------|--------------------------------------------------------------|\n", 4329 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4330 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4331 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4332 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4333 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4334 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4335 ]) 4336 4337 if margins[account]: 4338 info.extend([ 4339 "| Margin status: | Enabled |\n", 4340 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4341 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4342 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4343 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4344 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4345 ]) 4346 4347 else: 4348 info.append("| Margin status: | Disabled |\n\n") 4349 4350 info.extend([ 4351 "\n## Current user tariff limits\n", 4352 "\nSee also:\n", 4353 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4354 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4355 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4356 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4357 "\n### Unary limits\n", 4358 ]) 4359 4360 if unary: 4361 for key, values in sorted(unary.items()): 4362 info.append("\n* Max requests per minute: {}\n".format(key)) 4363 4364 for value in values: 4365 info.append(" - {}\n".format(value)) 4366 4367 else: 4368 info.append("\nNot available\n") 4369 4370 info.append("\n### Stream limits\n") 4371 4372 if stream: 4373 for key, values in sorted(stream.items()): 4374 info.append("\n* Max stream connections: {}\n".format(key)) 4375 4376 for value in values: 4377 info.append(" - {}\n".format(value)) 4378 4379 else: 4380 info.append("\nNot available\n") 4381 4382 infoText = "".join(info) 4383 4384 uLogger.info(infoText) 4385 4386 if self.userInfoFile: 4387 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4388 fH.write(infoText) 4389 4390 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4391 4392 return view
Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).
See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print user's data to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
4395class Args: 4396 """ 4397 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4398 """ 4399 def __init__(self, **kwargs): 4400 self.__dict__.update(kwargs) 4401 4402 def __getattr__(self, item): 4403 return None
If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.
4406def ParseArgs(): 4407 """This function get and parse command line keys.""" 4408 parser = ArgumentParser() # command-line string parser 4409 4410 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4411 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4412 4413 # --- options: 4414 4415 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4416 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4417 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4418 4419 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4420 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4421 4422 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4423 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4424 4425 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4426 4427 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4428 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4429 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4430 4431 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4432 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4433 4434 # --- commands: 4435 4436 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4437 4438 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4439 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4440 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4441 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4442 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4443 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4444 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4445 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4446 4447 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4448 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4449 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4450 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4451 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4452 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4453 4454 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4455 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4456 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4457 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4458 4459 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4460 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4461 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4462 4463 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4464 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4465 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4466 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4467 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4468 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4469 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4470 4471 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4472 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4473 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4474 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4475 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.") 4476 4477 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4478 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4479 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4480 4481 cmdArgs = parser.parse_args() 4482 return cmdArgs
This function get and parse command line keys.
4485def Main(**kwargs): 4486 """ 4487 Main function for work with TKSBrokerAPI in the console. 4488 4489 See examples: 4490 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4491 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4492 """ 4493 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4494 4495 if args.debug_level: 4496 uLogger.level = 10 # always debug level by default 4497 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4498 4499 exitCode = 0 4500 start = datetime.now(tzutc()) 4501 uLogger.debug("=-" * 50) 4502 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4503 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4504 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4505 )) 4506 4507 # trying to calculate full current version: 4508 buildVersion = __version__ 4509 try: 4510 v = version("tksbrokerapi") 4511 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4512 4513 except Exception: 4514 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4515 4516 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4517 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4518 4519 try: 4520 if args.version: 4521 print("TKSBrokerAPI {}".format(buildVersion)) 4522 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4523 4524 else: 4525 # Init class for trading with Tinkoff Broker: 4526 trader = TinkoffBrokerServer( 4527 token=args.token, 4528 accountId=args.account_id, 4529 useCache=not args.no_cache, 4530 ) 4531 4532 # --- set some options: 4533 4534 if args.more: 4535 trader.moreDebug = True 4536 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4537 4538 if args.ticker: 4539 ticker = args.ticker.upper() # Tickers may be upper case only 4540 4541 if ticker in trader.aliasesKeys: 4542 trader.ticker = trader.aliases[ticker] # Replace some tickers with its aliases 4543 4544 else: 4545 trader.ticker = ticker 4546 4547 if args.figi: 4548 trader.figi = args.figi.upper() # FIGIs may be upper case only 4549 4550 if args.depth is not None: 4551 trader.depth = args.depth 4552 4553 # --- do one command: 4554 4555 if args.list: 4556 if args.output is not None: 4557 trader.instrumentsFile = args.output 4558 4559 trader.ShowInstrumentsInfo(show=True) 4560 4561 elif args.list_xlsx: 4562 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4563 4564 elif args.bonds_xlsx is not None: 4565 if args.output is not None: 4566 trader.bondsXLSXFile = args.output 4567 4568 if len(args.bonds_xlsx) == 0: 4569 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4570 4571 else: 4572 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4573 4574 elif args.search: 4575 if args.output is not None: 4576 trader.searchResultsFile = args.output 4577 4578 trader.SearchInstruments(pattern=args.search[0], show=True) 4579 4580 elif args.info: 4581 if not (args.ticker or args.figi): 4582 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4583 raise Exception("Ticker or FIGI required") 4584 4585 if args.output is not None: 4586 trader.infoFile = args.output 4587 4588 if args.ticker: 4589 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4590 4591 else: 4592 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4593 4594 elif args.calendar is not None: 4595 if args.output is not None: 4596 trader.calendarFile = args.output 4597 4598 if len(args.calendar) == 0: 4599 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4600 4601 else: 4602 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4603 4604 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4605 4606 elif args.price: 4607 if not (args.ticker or args.figi): 4608 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4609 raise Exception("Ticker or FIGI required") 4610 4611 trader.GetCurrentPrices(show=True) 4612 4613 elif args.prices is not None: 4614 if args.output is not None: 4615 trader.pricesFile = args.output 4616 4617 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4618 4619 elif args.overview: 4620 if args.output is not None: 4621 trader.overviewFile = args.output 4622 4623 trader.Overview(show=True, details="full") 4624 4625 elif args.overview_digest: 4626 if args.output is not None: 4627 trader.overviewDigestFile = args.output 4628 4629 trader.Overview(show=True, details="digest") 4630 4631 elif args.overview_positions: 4632 if args.output is not None: 4633 trader.overviewPositionsFile = args.output 4634 4635 trader.Overview(show=True, details="positions") 4636 4637 elif args.overview_orders: 4638 if args.output is not None: 4639 trader.overviewOrdersFile = args.output 4640 4641 trader.Overview(show=True, details="orders") 4642 4643 elif args.overview_analytics: 4644 if args.output is not None: 4645 trader.overviewAnalyticsFile = args.output 4646 4647 trader.Overview(show=True, details="analytics") 4648 4649 elif args.overview_calendar: 4650 if args.output is not None: 4651 trader.overviewAnalyticsFile = args.output 4652 4653 trader.Overview(show=True, details="calendar") 4654 4655 elif args.deals is not None: 4656 if args.output is not None: 4657 trader.reportFile = args.output 4658 4659 if 0 <= len(args.deals) < 3: 4660 trader.Deals( 4661 start=args.deals[0] if len(args.deals) >= 1 else None, 4662 end=args.deals[1] if len(args.deals) == 2 else None, 4663 show=True, # Always show deals report in console 4664 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4665 ) 4666 4667 else: 4668 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4669 raise Exception("Incorrect value") 4670 4671 elif args.history is not None: 4672 if args.output is not None: 4673 trader.historyFile = args.output 4674 4675 if 0 <= len(args.history) < 3: 4676 dataReceived = trader.History( 4677 start=args.history[0] if len(args.history) >= 1 else None, 4678 end=args.history[1] if len(args.history) == 2 else None, 4679 interval="hour" if args.interval is None or not args.interval else args.interval, 4680 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 4681 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 4682 show=True, # shows all downloaded candles in console 4683 ) 4684 4685 if args.render_chart is not None and dataReceived is not None: 4686 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4687 4688 trader.ShowHistoryChart( 4689 candles=dataReceived, 4690 interact=iChart, 4691 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4692 ) 4693 4694 else: 4695 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4696 raise Exception("Incorrect value") 4697 4698 elif args.load_history is not None: 4699 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 4700 4701 if args.render_chart is not None and histData is not None: 4702 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4703 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 4704 4705 trader.ShowHistoryChart( 4706 candles=histData, 4707 interact=iChart, 4708 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4709 ) 4710 4711 elif args.trade is not None: 4712 if 1 <= len(args.trade) <= 5: 4713 trader.Trade( 4714 operation=args.trade[0], 4715 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 4716 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 4717 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 4718 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 4719 ) 4720 4721 else: 4722 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4723 4724 elif args.buy is not None: 4725 if 0 <= len(args.buy) <= 4: 4726 trader.Buy( 4727 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 4728 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 4729 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 4730 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 4731 ) 4732 4733 else: 4734 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4735 4736 elif args.sell is not None: 4737 if 0 <= len(args.sell) <= 4: 4738 trader.Sell( 4739 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 4740 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 4741 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 4742 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 4743 ) 4744 4745 else: 4746 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4747 4748 elif args.order: 4749 if 4 <= len(args.order) <= 7: 4750 trader.Order( 4751 operation=args.order[0], 4752 orderType=args.order[1], 4753 lots=int(args.order[2]), 4754 targetPrice=float(args.order[3]), 4755 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 4756 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 4757 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 4758 ) 4759 4760 else: 4761 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 4762 4763 elif args.buy_limit: 4764 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 4765 4766 elif args.sell_limit: 4767 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 4768 4769 elif args.buy_stop: 4770 if 2 <= len(args.buy_stop) <= 7: 4771 trader.BuyStop( 4772 lots=int(args.buy_stop[0]), 4773 targetPrice=float(args.buy_stop[1]), 4774 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 4775 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 4776 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 4777 ) 4778 4779 else: 4780 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4781 4782 elif args.sell_stop: 4783 if 2 <= len(args.sell_stop) <= 7: 4784 trader.SellStop( 4785 lots=int(args.sell_stop[0]), 4786 targetPrice=float(args.sell_stop[1]), 4787 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 4788 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 4789 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 4790 ) 4791 4792 else: 4793 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 4794 4795 # elif args.buy_order_grid is not None: 4796 # # update order grid work with api v2 4797 # if len(args.buy_order_grid) == 2: 4798 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 4799 # 4800 # for order in orderParams: 4801 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 4802 # 4803 # else: 4804 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4805 # 4806 # elif args.sell_order_grid is not None: 4807 # # update order grid work with api v2 4808 # if len(args.sell_order_grid) >= 2: 4809 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 4810 # 4811 # for order in orderParams: 4812 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 4813 # 4814 # else: 4815 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4816 4817 elif args.close_order is not None: 4818 trader.CloseOrders(args.close_order) # close only one order 4819 4820 elif args.close_orders is not None: 4821 trader.CloseOrders(args.close_orders) # close list of orders 4822 4823 elif args.close_trade: 4824 if not (args.ticker or args.figi): 4825 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4826 raise Exception("Ticker or FIGI required") 4827 4828 if args.ticker: 4829 trader.CloseTrades([args.ticker]) # close only one trade by ticker (priority) 4830 4831 else: 4832 trader.CloseTrades([args.figi]) # close only one trade by FIGI 4833 4834 elif args.close_trades is not None: 4835 trader.CloseTrades(args.close_trades) # close trades for list of tickers 4836 4837 elif args.close_all is not None: 4838 trader.CloseAll(*args.close_all) 4839 4840 elif args.limits: 4841 if args.output is not None: 4842 trader.withdrawalLimitsFile = args.output 4843 4844 trader.OverviewLimits(show=True) 4845 4846 elif args.user_info: 4847 if args.output is not None: 4848 trader.userInfoFile = args.output 4849 4850 trader.OverviewUserInfo(show=True) 4851 4852 elif args.account: 4853 if args.output is not None: 4854 trader.userAccountsFile = args.output 4855 4856 trader.OverviewAccounts(show=True) 4857 4858 else: 4859 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 4860 raise Exception("There is no command to execute") 4861 4862 except Exception: 4863 trace = tb.format_exc() 4864 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 4865 if e in trace: 4866 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 4867 break 4868 4869 uLogger.debug(trace) 4870 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 4871 exitCode = 255 # an error occurred, must be open a ticket for this issue 4872 4873 finally: 4874 finish = datetime.now(tzutc()) 4875 4876 if exitCode == 0: 4877 if args.more: 4878 uLogger.debug("All operations were finished success (summary code is 0).") 4879 4880 else: 4881 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 4882 os.path.abspath(uLog.defaultLogFile), exitCode, 4883 )) 4884 4885 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 4886 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 4887 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4888 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4889 )) 4890 uLogger.debug("=-" * 50) 4891 4892 if not kwargs: 4893 sys.exit(exitCode) 4894 4895 else: 4896 return exitCode
Main function for work with TKSBrokerAPI in the console.
See examples: